面试FQA
Dubbo
Q:dubbo基于的NIO 是一种什么技术?
Dubbo基于Netty,而Netty是一种基于NIO(Non-blocking I/O,非阻塞式I/O)的网络通信框架。NIO是Java提供的一种I/O模型,相对于传统的阻塞式I/O,NIO的主要优势在于它可以实现高并发和低延迟的网络通信,适用于处理大量连接和大量数据传输的场景。在NIO中,可以使用单线程处理多个连接,避免了线程切换的开销,提高了系统的性能和吞吐量。Netty是一个基于NIO的高性能网络通信框架,提供了易于使用的API和异步事件驱动的编程模型,被广泛应用于分布式系统、游戏、物联网等领域。
Q:dubbo面向服务的架构体系(SOA)
面向服务的架构体系(Service-Oriented Architecture,SOA)是一种软件设计和开发的方法论,其核心思想是将应用程序划分为一组可重用的、自治的、松耦合的服务,这些服务通过标准化的接口和协议进行通信和交互。SOA的目标是实现业务逻辑的模块化和复用,提高系统的灵活性、可扩展性和可维护性,同时降低开发和维护成本。SOA的实现需要使用一些技术和标准,如Web服务、XML、SOAP、WSDL等。SOA已经被广泛应用于企业级应用系统和分布式系统的开发中。
Q:Dubbo RPC协议的优点
以下是关于Dubbo RPC协议的详细介绍:Dubbo RPC协议是一种基于Java的远程过程调用协议,主要用于分布式系统中的服务调用。Dubbo的RPC协议采用了自定义的二进制编码和解码方式,可以通过配置来支持多种传输协议,如TCP、HTTP、Hessian、Dubbo协议等。Dubbo的RPC协议具有以下特点:
- 基于JavDubbo的RPC协议是基于Java语言实现的,可以与Java应用程序无缝集成,支持Java的序列化机制。
- 高效性:Dubbo的RPC协议采用了自定义的二进制编码和解码方式,相比于XML、JSON等文本格式,具有更高的传输效率和更小的网络开销。
- 可扩展性:Dubbo的RPC协议支持自定义的编解码器、传输协议、负载均衡策略等,可以根据具体的业务场景进行灵活配置和扩展。
- 高可用性:Dubbo的RPC协议支持多种负载均衡策略、容错机制和路由规则,可以实现高可用性的服务调用。
在Dubbo的RPC协议中,服务提供者将服务暴露到注册中心,服务消费者从注册中心获取服务提供者的地址信息,然后通过RPC协议进行远程调用。Dubbo的RPC协议可以实现跨语言的服务调用,只需要在服务提供者和服务消费者之间定义好协议和接口即可。
Q:dubbo的Monitorfilter
Dubbo的MonitorFilter是一种过滤器(Filter),用于收集Dubbo应用程序的运行信息和统计数据,并将这些数据发送到监控中心进行展示和分析。MonitorFilter通常与Dubbo的注册中心(zookepper)配合使用,可以将运行信息和统计数据发送到注册中心,然后由监控中心从注册中心获取这些数据。
MonitorFilter主要收集以下信息和数据:
- 调用次数:记录每个服务方法的调用次数,包括成功调用和失败调用。
- 调用时间:记录每个服务方法的调用时间,包括平均响应时间、最大响应时间和最小响应时间等。
- 错误次数:记录每个服务方法的错误次数,包括超时、异常、网络错误等。
- 调用者信息:记录调用者的IP地址、应用名称、服务名称等信息。
- 服务提供者信息:记录服务提供者的IP地址、应用名称、服务名称等信息。
MonitorFilter还支持自定义的数据收集和统计方式,可以通过实现MonitorFilter接口来扩展收集的数据和统计的方式。在使用MonitorFilter时,需要在Dubbo的配置文件中配置相应的参数,如监控中心的地址、应用名称、服务名称等。
Q:Dubbo 如何优雅停机?
Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果使用 kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行 JDK 的 ShutdownHook
JDK的ShutdownHook是一种Java虚拟机提供的机制,用于在JVM退出前执行一些操作,例如释放资源、清理缓存等。ShutdownHook可以在Java程序中注册一个或多个线程,在JVM退出时自动执行这些线程。
当JVM退出时,会按照注册的顺序依次执行ShutdownHook的run方法。ShutdownHook的执行时机为JVM即将退出时,可以是正常退出、异常退出或强制退出。在执行ShutdownHook期间,JVM会等待所有ShutdownHook线程执行完毕后再退出。
ShutdownHook的使用场景包括但不限于以下几种:
- 释放资源:例如关闭数据库连接、释放文件句柄等。
- 清理缓存:例如清理内存缓存、清理磁盘缓存等。
- 日志记录:例如记录JVM退出时间、异常信息等。
需要注意的是,ShutdownHook的执行时间是有限制的,不能太长,否则会影响JVM退出的时间。另外,ShutdownHook的执行顺序是不确定的,不同的JVM实现可能有不同的执行顺序。因此,在编写ShutdownHook时,需要考虑执行时间和执行顺序的影响,尽量保证ShutdownHook的执行时间短,避免依赖执行顺序。
Spring
Q: Spring Cloud Netflix Eureka
Spring Cloud Netflix Eureka是由Netflix公司出品的开源项目,用于实现服务注册与发现功能。Netflix公司是一家美国的在线视频服务提供商,也是一家以开源软件为主要业务的公司。Netflix公司在构建自己的微服务架构时,开发了很多优秀的开源项目,例如Eureka、Hystrix、Zuul等,这些项目都被整合到了Spring Cloud Netflix中,成为了Spring Cloud生态系统中的重要组成部分。
Spring Cloud Netflix是Spring Cloud项目中的一个子项目,提供了对Netflix公司开源的一些微服务组件的封装和整合,包括Eureka、Hystrix、Zuul、Archaius等。Spring Cloud Netflix提供了一种简单、易于使用和集成的微服务架构解决方案,可以快速构建分布式系统和微服务应用程序。
需要注意的是,Netflix公司已经于2021年宣布停止对Eureka、Hystrix等项目的维护,建议用户转向使用Spring Cloud Alibaba等其他解决方案。不过,由于Eureka等项目已经成为了开源社区中的重要组成部分,仍然可以继续使用和开发。
Spring Cloud Netflix Eureka是Spring Cloud Netflix项目中的一个模块,用于实现服务注册与发现功能。Eureka提供了一个基于REST的服务注册与发现的解决方案,可以使微服务架构中的服务实例自动注册到Eureka Server,并实现服务之间的自动发现和负载均衡。以下是Eureka的主要特点:
- 服务注册与发现:Eureka提供了服务注册与发现的功能,服务提供者可以将自己的地址信息注册到Eureka Server,服务消费者可以从Eureka Server中获取服务提供者的地址信息,实现服务之间的自动发现和负载均衡。
- 高可用性:Eureka Server可以搭建成集群,实现高可用性的服务注册与发现。Eureka Server之间可以相互注册,实现服务信息的互通和负载均衡。
- 心跳机制:Eureka通过心跳机制来监控服务实例的健康状态,如果服务实例长时间没有发送心跳,则Eureka会将该实例从服务列表中剔除。
- 自我保护机制:Eureka提供了自我保护机制,当Eureka Server节点之间出现网络故障或宕机时,Eureka会进入自我保护模式,保证服务注册与发现的正常运行。
- Spring Cloud集成:Eureka可以与Spring Cloud集成,通过配置简单的注解或属性即可实现服务注册与发现的功能。
使用Eureka进行服务注册与发现的步骤包括:
- 启动Eureka Server:搭建Eureka Server集群,启动Eureka Server实例。
- 注册服务:在服务提供者的应用程序中,配置Eureka Client,将服务实例的地址信息注册到Eureka Server。
- 发现服务:在服务消费者的应用程序中,配置Eureka Client,从Eureka Server中获取服务提供者的地址信息,实现服务调用和负载均衡。
Spring Cloud Netflix Eureka的优点在于它提供了一种简单、轻量级、易于使用和集成的服务注册与发现的解决方案,可以快速构建微服务架构。同时,Eureka还提供了心跳机制、自我保护机制等高可用性的功能,可以保证服务的稳定性和可靠性。
Q:Spring Cloud Alibaba 的前世今生
Spring Cloud Alibaba是Spring Cloud和Alibaba合作开发的一套微服务框架解决方案,它提供了一系列的开源组件和工具,帮助开发者构建分布式应用程序和微服务架构。Spring Cloud Alibaba的前世今生可以概括如下:
- 前世
在Spring Cloud出现之前,Spring框架已经成为Java开发的事实标准,但是在分布式应用程序和微服务架构方面,Spring框架缺乏完整的解决方案。此时,阿里巴巴开始在自己的业务中探索微服务架构,并开发了一些解决方案,例如Dubbo、Sentinel等。这些解决方案在阿里巴巴内部得到了广泛使用,并逐渐开源,成为了开源社区中的重要组成部分。
随着Spring Cloud的出现,Spring和阿里巴巴开始合作,将Dubbo、Sentinel等开源组件整合到了Spring Cloud中,形成了Spring Cloud Alibaba。
- 今生
Spring Cloud Alibaba提供了一系列的开源组件和工具,包括:
- Nacos:服务注册与发现、配置管理、流量管理的平台,替代了Eureka、Config Server、Ribbon等组件。
- Sentinel:熔断器、限流器、降级器的框架,替代了Hystrix等组件。
- RocketMQ:分布式消息中间件,替代了Kafka等组件。
- Seat分布式事务解决方案,替代了Atomikos等组件。
- Alibaba Cloud OSS、SMS、OSS等:阿里云对象存储、短信服务、日志服务等。
Spring Cloud Alibaba的优点在于它提供了一系列的开源组件和工具,可以构建完整的微服务架构解决方案。同时,Spring Cloud Alibaba也与Spring Cloud集成,可以与Spring生态系统中的其他组件一起使用,例如Spring Boot、Spring Cloud Gateway等,具有较高的灵活性和可扩展性。
RocketMQ
Q:RocketMQ 和 RabbitMQ 的详细介绍区别
RocketMQ和RabbitMQ都是开源的消息中间件,用于实现分布式系统中的异步消息传递。它们的主要区别如下:
- 架构设计
RocketMQ采用了分布式的Broker架构,消息存储在Broker节点上。RocketMQ的Broker节点可以横向扩展,支持高可用性和负载均衡。
RabbitMQ采用了AMQP协议,采用的是中心化的Broker架构,消息存储在Broker节点上。RabbitMQ的Broker节点可以横向扩展,支持高可用性和负载均衡。
- 性能
RocketMQ在性能方面表现较好,可以支持高并发、高吞吐量的消息传递,适合于大规模的分布式系统。
RabbitMQ在性能方面表现较差,不适合于高并发、高吞吐量的消息传递。
- 可靠性
RocketMQ在可靠性方面表现较好,支持消息的可靠传递和事务消息,可以保证消息的不丢失和不重复。
RabbitMQ在可靠性方面表现一般,支持消息的可靠传递,但不支持事务消息,可能会出现消息的丢失或重复。
- 社区支持
RocketMQ的社区支持相对较少,但是随着阿里巴巴的开源战略,近年来得到了更多的关注和支持。
RabbitMQ的社区支持相对较多,得到了广泛的应用和支持。
总的来说,RocketMQ和RabbitMQ都是优秀的消息中间件,可以根据具体的应用场景和需求选择。如果需要高可靠性、高性能、支持事务消息等特性,可以选择RocketMQ;如果需要更多的社区支持、更加灵活的架构设计等特性,可以选择RabbitMQ。
Q:RocketMQ 优势
Broker架构是一种常见的消息中间件架构,它将整个消息服务划分为两个部分:消息生产者和消息消费者。消息生产者将消息发送到Broker节点,Broker节点将消息存储在队列中,消息消费者从队列中消费消息。
Broker架构通常包含以下三个组件:
- Producer:消息生产者,负责将消息发送到Broker节点。Producer可以同时向多个Broker节点发送消息,以提高消息传递的可靠性和性能。
- Broker:消息中间件的核心组件,负责消息的存储和传递。Broker节点通常是一个分布式集群,可以横向扩展以支持更高的并发和吞吐量。Broker节点之间通过网络进行消息传递和负载均衡,以实现系统的可靠性、高可用性和扩展性。
- Consumer:消息消费者,负责从Broker节点消费消息。Consumer可以订阅一个或多个Topic,并从对应的队列中消费消息。Consumer可以同时从多个Broker节点消费消息,以提高消息传递的可靠性和性能。
Broker架构的优点包括:
- 可靠性:Broker架构通过将消息存储在队列中,可以保证消息的可靠传递和不丢失。同时,通过多个Broker节点之间的消息传递和负载均衡,可以提高系统的可靠性和高可用性。
- 扩展性:Broker架构可以根据业务需求动态添加或移除Broker节点,以适应不断增长的消息传递量和并发访问量。同时,Broker节点之间的负载均衡机制可以提高系统的扩展性和性能。
- 灵活性:Broker架构可以适用于各种不同的应用场景,可以根据业务需求进行定制和扩展。同时,Broker架构可以与不同的编程语言和技术栈进行集成,以提高开发效率和灵活性。
总的来说,Broker架构是一种可靠、扩展、灵活的消息中间件架构,广泛应用于分布式系统中的消息传递和异步通信。
Q:RocketMQ 在电商行业的优势
RocketMQ是一个高性能、可靠、可扩展的分布式消息中间件,适合在电商行业等需要大规模消息传递的场景中使用。以下是RocketMQ在电商行业中的一些应用场景和优势:
- 订单处理和支付:电商行业的核心业务是订单处理和支付,通过RocketMQ可以实现订单状态的实时更新和支付结果的及时通知,保证订单的及时处理和支付的可靠性。
- 库存管理和物流配送:电商行业需要对商品库存进行实时管理,并及时通知物流配送系统进行发货,通过RocketMQ可以实现库存和物流系统之间的实时同步,保证库存和物流的及时性和准确性。
- 营销推广和用户关系管理:电商行业需要进行营销推广和用户关系管理,通过RocketMQ可以实现实时推送营销信息和用户关系变化通知,提高用户体验和粘性。
- 报表分析和数据挖掘:电商行业需要进行报表分析和数据挖掘,通过RocketMQ可以实现数据采集和实时传递,提高数据分析和决策的效率和准确性。
RocketMQ在电商行业中的优势主要有以下几点:
- 高性能和可靠性:RocketMQ具有高性能和可靠性的特点,可以满足电商行业对消息传递的高并发、高吞吐量和可靠性的要求。
- 可扩展性和可定制性:RocketMQ具有良好的可扩展性和可定制性,可以根据电商行业的业务需求进行定制和扩展。
- 易于使用和集成:RocketMQ具有良好的易用性和集成性,可以与电商行业中常用的技术和框架进行集成,例如Spring、MyBatis等。
综上所述,RocketMQ是一个适合在电商行业中使用的高性能、可靠、可扩展的消息中间件,可以满足电商行业对消息传递的高并发、高吞吐量和可靠性的要求,提高电商系统的效率、稳定性和用户体验。
elasticsearch
Q:roll over API 滚动索引
Roll over API(滚动索引)是Elasticsearch提供的一种机制,用于解决索引数据量过大导致查询性能下降的问题。滚动索引可以将一个大的索引分成多个小的索引,每个小索引存储一段时间范围内的数据,然后通过滚动的方式逐步将新的数据写入新的小索引,同时删除旧的小索引,从而保持索引的大小在可控范围内。
滚动索引的主要作用是:
- 提高查询性能:当一个索引变得过大时,查询性能会降低,因为Elasticsearch需要扫描更多的数据。通过滚动索引,可以将索引分成多个小的索引,每个小索引的数据量较小,查询性能更高。
- 优化存储空间:当一个索引变得过大时,索引文件的存储空间也会变得很大。通过滚动索引,可以将索引分成多个小的索引,每个小索引的存储空间更小,可以更好地利用存储资源。
- 管理索引数据:通过滚动索引,可以将索引数据按时间范围进行分段管理,便于数据的维护和管理。同时,也可以方便地实现数据的归档和备份等操作。
总的来说,滚动索引是一种重要的索引管理机制,可以帮助优化Elasticsearch的性能和存储空间,同时也可以方便地管理索引数据。
Q:elasticsearch 集群架构
Elasticsearch是一个分布式搜索和分析引擎,支持构建高可用性、可扩展性和容错性的集群架构。以下是Elasticsearch集群架构的主要组件和特点:
- 节点(Node):Elasticsearch集群由多个节点组成,每个节点都是一个独立的Elasticsearch实例。节点之间通过网络进行通信和数据传输,以实现分布式的搜索和分析。
- 索引(Index):Elasticsearch将数据存储在索引中,每个索引可以包含多个分片(Shard)和副本(Replica)。索引的分片和副本被分布在不同的节点上,以实现数据的分布式存储和高可用性。
- 分片(Shard):Elasticsearch将索引分成多个分片,每个分片存储部分数据。分片的数量可以根据数据量和性能需求进行调整,以实现数据的横向扩展和负载均衡。
- 副本(Replica):Elasticsearch可以为每个分片创建多个副本,副本的数量可以根据数据可靠性和查询性能需求进行调整。副本可以提高数据的可靠性和查询性能,同时也可以实现故障转移和负载均衡。
- 集群(Cluster):Elasticsearch集群由多个节点组成,每个节点都是一个独立的Elasticsearch实例。节点之
Elasticsearch的集群架构具有以下特点:
- 分布式:Elasticsearch集群采用分布式架构,可以将数据分散存储在多个节点上,以实现数据的横向扩展和负载均衡。
- 高可用性:Elasticsearch集群支持多副本机制,可以实现数据的备份和故障转移,从而提高系统的可用性和容错性。
- 可扩展性:Elasticsearch集群可以根据数据量和性能需求进行横向扩展,可以动态添加或删除节点、分片和副本,以适应不断增长的数据量和访问量。
- 自动发现:Elasticsearch集群支持自动发现机制,可以自动识别新加入或退出的节点,从而实现集群的自动管理和维护。
总的来说,Elasticsearch的集群架构具有高可用性、可扩展性和容错性等优点,可以应用于各种分布式搜索和分析场景。
Q:query then fetch 是什么意思
Query-Then-Fetch是Elasticsearch用于执行分布式搜索的一种搜索模式。它首先在所有节点上执行查询操作,然后将查询结果传输回协调节点进行排序、分页和聚合等操作,最后将最终结果返回给客户端。
具体来说,Query-Then-Fetch的搜索过程分为两个步骤:
- Query:客户端向协调节点发送查询请求,协调节点将查询请求转发给所有节点,并在每个节点上执行查询操作。每个节点将查询结果返回给协调节点。
- Fetch:协调节点将所有节点返回的查询结果进行排序、分页和聚合等操作,并将最终结果返回给客户端。
Query-Then-Fetch是Elasticsearch默认的搜索模式,它具有以下优点:
- 并行查询:Query-Then-Fetch可以在所有节点上并行执行查询操作,从而提高搜索性能和响应速度。
- 分布式排序:Query-Then-Fetch可以将所有节点返回的查询结果进行分布式排序,从而提高排序性能和可扩展性。
- 分布式聚合:Query-Then-Fetch可以将所有节点返回的查询结果进行分布式聚合,从而提高聚合性能和可扩展性。
- 网络传输优化:Query-Then-Fetch可以通过网络传输优化,减少不必要的数据传输和网络开销,从而提高搜索性能和可扩展性。
总的来说,Query-Then-Fetch是Elasticsearch的一种高效、可扩展的分布式搜索模式,可以应用于各种搜索场景。
Q:lucence 内部结构是什么
Lucene是一个高性能、可扩展、全文搜索引擎库,其内部结构包括以下几个主要组件:
- 索引(Index):Lucene将文档数据存储在索引中,每个索引包含多个段(Segment),每个段包含多个文档(Document)和词条(Term)。
- 分析器(Analyzer):Lucene使用分析器将文本数据分解为词条,分析器可以根据具体需求进行定制和扩展。
- 倒排索引(Inverted Index):Lucene使用倒排索引存储词条和文档之间的关系,倒排索引可以快速地定位到包含特定词条的文档。
- 检索器(Searcher):Lucene使用检索器进行搜索操作,检索器可以在索引中查找包含特定词条的文档,并计算文档的相关度得分。
- 查询(Query):Lucene使用查询对象进行搜索操作,查询对象可以包含一个或多个查询子句(Clause),每个查询子句可以指定一个或多个词条进行匹配。
- 相关度得分(Score):Lucene使用相关度得分计算文档与查询的匹配程度,相关度得分会影响搜索结果的排序和排名。
总的来说,Lucene的内部结构是一个基于倒排索引的搜索引擎库,它具有高性能、可扩展、全文搜索和相关度排序等特点,可以应用于各种搜索场景。
Q:倒排索引
倒排索引(Inverted Index)是一种用于快速搜索和定位文档的数据结构,它将文档中的词条和文档之间的关系反转,从而允许根据词条快速定位到包含该词条的文档。
具体来说,倒排索引包括以下两个主要组成部分:
- 词条表(Term Dictionary):词条表记录了所有出现过的词条及其在倒排索引中的位置信息,包括词条出现的文档数、文档列表的指针和词条的位置等信息。
- 倒排列表(Posting List):倒排列表记录了每个词条在哪些文档中出现过,包括文档的编号、词条在文档中的位置和词条的频率等信息。
倒排索引的搜索过程通常包括以下几个步骤:
- 分析查询语句:将查询语句分解为词条,并对词条进行处理和归一化,以便于和倒排索引中的词条进行匹配。
- 查找倒排列表:在词条表中查找查询词条的位置,并获取倒排列表的指针。
- 合并倒排列表:将多个倒排列表进行合并,并计算文档与查询的相关度得分。
- 排序和返回结果:根据文档的相关度得分对搜索结果进行排序,并返回前N个最相关的文档。
倒排索引是现代搜索引擎的核心技术之一,具有高效、可扩展、全文搜索和相关度排序等优点,可以应用于各种搜索场景。
Q:常见数据结构
Q:Array/List 使用的是二分查找,不平衡,这个不平衡指的是什么
Array/List使用的二分查找是一种基于比较的查找算法,它通过将查找区间不断缩小一半的方式来快速定位目标元素。在二分查找中,查找区间被划分为两个部分,每次比较后将查找区间缩小到目标元素可能存在的那一半,直到查找到目标元素或者查找区间为空为止。
然而,在Array/List中使用二分查找时,如果待查找的元素在数组中的分布不均匀,即元素的分布密度不一致,就可能导致查找区间的不平衡。具体来说,如果待查找的元素在数组的左侧或右侧分布密度较高,那么每次查找时都会选择与目标元素更接近的一侧继续查找,这样就会导致查找区间不断向某一侧偏移,最终可能导致查找效率降低。
例如,在一个有序的数组中查找一个元素时,如果该元素在数组的中间位置,那么每次查找时都可以将查找区间缩小为原来的一半,这样可以快速定位目标元素。但是,如果待查找的元素在数组的左侧或右侧分布密度较高,那么每次查找时都会选择与目标元素更接近的一侧继续查找,这样就会导致查找区间不断向某一侧偏移,最终可能导致查找效率降低。
为了避免这种情况,可以采用一些优化策略,例如使用平衡查找树(如红黑树、AVL树等)来代替数组或列表,或者使用一些特殊的算法来处理不均匀分布的数据。
Q:trie 树
Trie树(也称为字典树或前缀树)是一种基于树结构的数据结构,用于高效地存储和检索字符串集合。Trie树的基本思想是将每个字符串拆分成一个个字符,然后将这些字符按照顺序存储在一棵树中。Trie树的根节点表示空字符串,每个节点表示一个字符,从根节点到叶子节点的路径表示一个完整的字符串。
Trie树的主要特点包括:
- 前缀匹配:Trie树可以高效地查找所有以某个前缀开头的字符串,因为所有以该前缀开头的字符串都会共享相同的前缀路径。
- 空间效率:Trie树可以通过共享公共前缀来节省空间,因为多个字符串的前缀可能是相同的。
- 时间效率:Trie树的时间复杂度与字符串长度相关,因为需要遍历每个字符,但是可以通过优化算法和数据结构来提高查询效率。
Trie树的基本操作包括插入、查找、删除等,插入操作将一个新的字符串插入到Trie树中,查找操作可以快速地判断一个字符串是否存在于Trie树中,删除操作可以删除一个已经存在的字符串。
Trie树的应用非常广泛,例如搜索引擎中的关键词匹配、拼写检查、字符串匹配等领域。
Q:Skip List(跳表)
Skip List(跳表)是一种基于链表的数据结构,用于高效地支持动态集合操作,如查找、插入和删除。Skip List的基本思想是通过添加多级索引来加速链表的查找操作,从而实现较高的查询效率。
Skip List的每一层都是一个有序链表,第0层是原始链表,每一级的节点都包含一个指向下一层节点的指针。Skip List的每一层都是按照一定概率(通常为1/2)随机生成的,因此每一层的节点数都是不确定的。每个节点都包含一个关键字和一个指向下一个节点的指针,同时还可以包含一些附加的信息。
Skip List的查找操作从最高层开始,沿着每一层的链表进行搜索,如果当前节点的关键字小于目标值,则继续向右移动;如果当前节点的关键字大于目标值,则向下移动到下一层继续搜索。当找到目标值或者到达最后一层时,查找操作结束。
Skip List的插入和删除操作也非常简单,插入操作首先执行查找操作,然后在每一层中插入新的节点,并更新相应的指针;删除操作则是将要删除的节点从每一层中移除,并更新相应的指针。
Skip List的优点是实现简单、易于扩展、查询效率较高,但是其缺点是占用较多的空间,因为每个节点都需要维护多个指针。为了解决这个问题,可以使用一些优化技术,如压缩指针、使用双向跳表等。Skip List的应用非常广泛,例如Redis中的有序集合就是使用Skip List来实现的。
Memcached
Q:Memcached 是什么,有什么作用
Memcached是一种高性能的分布式内存对象缓存系统,可以用于缓存各种类型的数据,如数据库查询结果、API调用结果、模板渲染结果等。Memcached的主要作用是加速Web应用程序的数据访问速度,从而提高Web应用程序的性能和可扩展性。
Memcached的基本原理是将数据缓存在内存中,以提高数据的访问速度。当应用程序需要访问数据时,首先会尝试从Memcached中读取数据,如果数据存在则直接返回结果,否则将从数据库或其他数据源中读取数据,并将结果缓存到Memcached中,以供下次访问使用。
Memcached的优点包括:
- 高性能:Memcached可以高效地缓存数据,从而提高数据的访问速度,减少数据库等数据源的负载。
- 可扩展性:Memcached支持分布式部署,可以将数据缓存在多个节点上,从而提高系统的可扩展性和容错性。
- 简单易用:Memcached的接口简单易用,可以通过多种编程语言和API来访问缓存数据。
- 可靠性:Memcached可以通过多种机制来保证数据的可靠性和一致性,如数据备份、数据失效机制等。
Memcached的应用非常广泛,例如在电子商务网站中,可以使用Memcached来缓存商品信息、购物车信息、用户信息等,从而提高网站的访问速度和性能。在社交网络应用中,可以使用Memcached来缓存用户信息、好友信息、消息等,从而提高应用程序的性能和可扩展性。
Q:memcached 服务在企业集群架构中有哪些应用场景?
一、作为数据库的前端缓存应用
a、完整缓存(易),静态缓存
例如:商品分类(京东),以及商品信息,可事先放在内存里,然后再对外提供
数据访问,这种先放到内存,我们称之为预热,(先把数据存缓存中),用户访
问时可以只读取 memcached 缓存,不读取数据库了。
b、执点缓存(难)
需要前端 web 程序配合,只缓存热点的数据,即缓存经常被访问的数据。
先预热数据库里的基础数据,然后在动态更新,选读取缓存,如果缓存里没有对
应的数据,程序再去读取数据库,然后程序把读取的新数据放入缓存存储。
特殊说明 :
-
如果碰到电商秒杀等高并发的业务,一定要事先预热,或者其它思想实现,
例如:称杀只是获取资格,而不是瞬间秒杀到手商品。
那么什么是获取资格? -
就是在数据库中,把 0 标成 1.就有资格啦。再慢慢的去领取商品订单。
因为秒杀过程太长会占用服务器资源。 -
如果数据更新,同时触发缓存更新,防止给用户过期数据。
-
对于持久化缓存存储系统,例如:redis,可以替代一部分数据库的存储,
一些简单的数据业务,投票,统计,好友关注,商品分类等。nosql= not only sqlQ:单点登录
单点登录(Single Sign-On,简称 SSO)是一种身份认证技术,其原理是让用户只需登录一次,就可以在多个系统或应用程序中访问受保护的资源,而无需再次输入凭据或进行身份验证。
在 SSO 中,用户首先登录到一个称为身份提供者(Identity Provider,简称 IdP)的中央认证系统。一旦用户通过身份验证,IdP 将颁发一个令牌(Token),该令牌可以用于在多个不同的应用程序中访问资源。当用户尝试访问另一个应用程序时,该应用程序将发送一个请求到 IdP,以验证用户的令牌。如果令牌有效,则用户将被认为已经通过身份验证,并且可以访问该应用程序中的资源。
SSO 的主要作用是提高用户体验和安全性。通过减少用户需要输入凭据的次数,SSO 可以提高用户的工作效率和满意度。此外,SSO 还可以减少密码重用和弱密码等安全风险,因为用户只需记住一个密码即可。
Q:Memcached 异步I/O 模型,使用 libevent 作为事件通知机制
libevent 是一个基于事件驱动的网络编程库,它提供了一种高效的事件通知机制,可以在不阻塞线程的情况下处理大量的并发连接。libevent 主要用于编写高性能的网络服务器和客户端程序,可以在 Linux、Mac OS X、FreeBSD、OpenBSD、NetBSD 等操作系统上运行。
libevent 的核心原理是基于事件循环机制,通过监听各种事件(如网络事件、定时器事件等),并在事件触发时执行相应的回调函数。与传统的多线程或多进程编程模型相比,libevent 可以大幅度减少线程或进程的数量,从而提高应用程序的性能和可扩展性。
Q:LRU(Least Recently Used)算法
LRU(Least Recently Used)算法是一种常见的缓存淘汰策略,它的核心思想是基于时间局部性原理,即最近被使用的数据很可能在未来一段时间内会被再次使用。因此,LRU 算法会优先淘汰最近最少使用的缓存数据,以保留最常使用的数据。
具体来说,LRU 算法会维护一个缓存数据访问顺序的链表(或双向链表),每次访问一个缓存数据时,就将其移到链表头部。当需要淘汰缓存数据时,就从链表尾部开始查找最近最少使用的缓存数据,并将其从缓存中删除。
例如,假设有一个缓存容量为 3 的缓存,当前缓存中存储了数据 A、B、C,访问顺序为 A->B->C,此时需要添加一个新的数据 D,由于缓存容量已满,需要淘汰一个旧的数据。根据 LRU 算法,应该淘汰最近最少使用的数据,即链表尾部的数据 A,然后将新的数据 D 添加到链表头部,此时缓存中存储的数据为 D->B->C。
LRU 算法可以通过哈希表和双向链表的结合来实现,哈希表用于快速查找缓存数据,双向链表用于维护缓存数据的访问顺序。LRU 算法在实际应用中非常广泛,例如在数据库缓存、Web 服务器缓存、操作系统页面置换等方面都有应用。
Q:计算机领域的 ”惊群”问题 举例说明
计算机领域中的 "惊群"问题通常指在某个事件发生时,多个进程或线程同时竞争资源,导致系统性能下降或崩溃的现象。一个典型的例子是网络服务器上的 "惊群"问题。
假设一个网络服务器上有多个进程或线程等待接收客户端请求。当一个客户端请求到达时,操作系统会通知所有等待请求的进程或线程,然后它们会竞争获取这个请求并进行处理。如果这些进程或线程没有正确地协调和同步,它们可能会同时尝试获取请求并进行处理,导致资源竞争和性能下降。
为了避免这种问题,通常的解决方案是使用互斥锁、条件变量等同步机制来确保只有一个进程或线程获取请求并进行处理,其他进程或线程等待。这样可以避免资源竞争和性能下降,提高系统的可靠性和性能。
Q:obscure机制
"obscure" 是一个英文单词,指的是模糊、不清楚、不易理解的意思。在计算机领域,"obscure" 通常用来描述一些机制或算法,它们的实现细节比较复杂或是不太容易理解。
例如,一些密码学算法就可能会被认为是 "obscure" 的,因为它们的实现细节非常复杂,需要深入了解密码学原理才能理解其工作原理。又比如说,一些复杂的调试工具或性能分析工具,也可能会被认为是 "obscure" 的,因为它们的实现细节非常复杂,需要深入了解计算机系统和操作系统的原理才能理解其工作原理。
尽管这些机制或算法可能比较复杂或不容易理解,但它们在计算机领域中具有非常重要的作用,能够帮助开发人员和系统管理员更好地管理和维护计算机系统,保障系统的安全和稳定性。
Q:常说一个技术有原子性指的是什么
当我们说一个技术有原子性时,通常指这个技术可以被拆分成一些独立的、不可再分的、最小的单元。这些单元可以被独立地实现、测试、部署和维护,而不会对其他单元产生影响。
具有原子性的技术通常可以更容易地进行模块化和重用,提高代码的可维护性和可扩展性。每个原子单元都可以被视为一个独立的模块,可以在不影响其他模块的情况下进行修改和优化。这有助于降低代码的复杂度和风险,提高代码的可靠性和稳定性。
在软件开发中,常见的具有原子性的技术包括函数、方法、类、模块等。在系统设计和架构中,也可以使用具有原子性的技术来实现模块化和分层设计,例如微服务架构中的服务、容器化技术中的镜像等。
Q:为了解决集群环境下的 seesion 共享问题,共有 4 种解决方案:
1.粘性 session
粘性 session 是指 Ngnix 每次都将同一用户的所有请求转发至同一台服务器上,
即将用户与服务器绑定。
2.服务器 session 复制
即每次 session 发生变化时,创建或者修改,就广播给所有集群中的服务器,使
所有的服务器上的 session 相同。
3.session 共享
缓存 session,使用 redis, memcached。
4.session 持久化
将 session 存储至数据库中,像操作数据一样才做 session。
Redis
Q:master-slave 主从复制模式的数据备份
Master-slave模式是一种常见的数据库备份方案,也被称为主从复制。在这种模式下,主数据库(Master)是主要的数据库服务器,负责处理所有的读写请求。而从数据库(Slave)则是主数据库的备份,它通过复制主数据库的数据来保持与主数据库的同步。
在主从复制中,主数据库将所有的写操作记录在二进制日志(Binary Log)中,并将日志文件发送给从数据库。从数据库则通过读取主数据库的二进制日志来保持与主数据库的同步。因此,当主数据库出现故障或数据丢失时,可以通过从数据库来进行数据恢复。
主从复制模式的数据备份具有以下优点:
- 数据备份实时性高:由于主从复制是实时的,从数据库会及时地复制主数据库的数据,因此可以保证数据备份的实时性和准确性。
- 数据可靠性高:主从复制模式可以保证数据的可靠性。当主数据库出现故障或数据丢失时,可以通过从数据库来进行数据恢复。
- 负载均衡:主从复制模式可以实现负载均衡,将读请求分散到从数据库中处理,减轻主数据库的压力,提高系统的性能和稳定性。
总的来说,主从复制模式是一种可靠的数据备份方案,可以保证数据的实时性和可靠性,同时还可以实现负载均衡,提高系统的性能和稳定性。
Q:在Redis中,常见的数据类型有list、set、zset和hash四种,它们各有不同的特点和用途。
- List(列表):List是一个有序的字符串列表,可以添加、删除、插入和访问列表中的元素。在Redis中,List是一个双向链表,可以在头部或尾部进行插入和删除操作,也可以根据索引访问列表中的元素。List常用于实现队列、栈、消息队列等场景。
- Set(集合):Set是一个无序的字符串集合,可以添加、删除、查找和计算集合中的元素。Set中的元素是唯一的,重复的元素会被自动去重。Set常用于实现去重、交集、并集、差集等场景。
- Zset(有序集合):Zset是一个有序的字符串集合,每个元素都关联一个分数(score),可以按照分数进行排序、查询和计算。Zset中的元素是唯一的,重复的元素会被自动去重。Zset常用于实现排行榜、计分系统、范围查询等场景。
- Hash(哈希表):Hash是一个键值对集合,可以添加、删除、查找和修改哈希表中的元素。在Redis中,Hash使用了类似于Java中的HashMap的实现方式,可以实现O(1)级别的访问和修改操作。Hash常用于实现对象、配置、统计等场景。
总的来说,Redis的不同数据类型各有不同的特点和应用场景。根据实际需求选择合适的数据类型可以使代码更简洁、性能更高效。
Q:O(1)
O(1)是指算法的时间复杂度为常数级别,也就是说算法的执行时间不随输入规模的增加而增加,是一种非常高效的算法。在HashMap中,查找和操作的时间复杂度都是O(1),也就是说无论HashMap中存储了多少元素,查找和操作的时间都是不变的,因此HashMap是一种非常高效的数据结构。
Q:是否使用过 Redis 集群,集群的原理是什么?
1)、Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为
master,继续提供服务。
2)、Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行
分片存储
Q:redis事务中的 MULTI、EXEC、DISCARD、WATCH 几个命令如何理解,详细描述下
Redis中的事务是指一系列的命令操作被当作一个单独的操作单元来执行。Redis事务使用MULTI、EXEC、DISCARD和WATCH等命令来实现。以下是这些命令的详细描述:
- MULTI命令:MULTI命令用于开启一个事务。执行MULTI命令后,Redis会将接下来的所有命令都缓存起来,直到执行EXEC命令时才会一次性地执行这些命令。如果在MULTI和EXEC之间出现了错误,可以使用DISCARD命令来取消事务。
- EXEC命令:EXEC命令用于执行一个事务中缓存的所有命令。执行EXEC命令后,Redis会将事务中的所有命令依次执行,并将执行结果返回给客户端。如果事务中的任意一个命令执行失败,整个事务都会被回滚。
- DISCARD命令:DISCARD命令用于取消一个事务。执行DISCARD命令后,Redis会清空当前事务中所有缓存的命令,并将事务状态重置为未开启状态。
- WATCH命令:WATCH命令用于监视一个或多个键,如果在事务执行期间,任意一个被监视的键被修改,整个事务就会被回滚。可以使用UNWATCH命令来取消对键的监视。
在Redis中,事务是基于乐观锁实现的,WATCH命令可以用来监视被事务所涉及的键,如果在事务执行期间,被监视的键发生了变化,事务就会被回滚。这种方式比传统的悲观锁更加高效,因为它不需要在事务开始时就进行加锁操作。但是,需要注意的是,由于WATCH命令只能监视被事务所涉及的键,因此如果事务中的操作涉及到多个键,需要在事务开始前使用WATCH命令对这些键进行监视。
MySQL
Q:MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?
1、设计良好的数据库结构,允许部分数据冗余,尽量避免 join 查询,提高效率。
2、选择合适的表字段数据类型和存储引擎,适当的添加索引。
3、MySQL 库主从读写分离。
4、找规律分表,减少单表中的数据量提高查询速度。
5、添加缓存机制,比如 memcached,apc 等。
6、不经常改动的页面,生成静态页面。
7、书写高效率的 SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1,
field_2, field_3 FROM TABLE.
Q:MySQL锁的优化策略
1、读写分离
2、分段加锁
3、减少锁持有的时间
4.多个线程尽量以相同的顺序去获取资源
不能将锁的粒度过于细化,不然可能会出现线程的加锁和释放次数过多,反而效率不如一次加一把大锁
Q:什么情况下设置了索引但无法使用
1、以“%”开头的 LIKE 语句,模糊匹配
2、OR 语句前后没有同时使用索引
3、数据类型出现隐式转化(如 varchar 不加单引号的话可能会自动转换为 int 型)
Q:优化数据库的方法
1、选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置 NOTNULL,例如’省份’、’性别’最好适用 ENUM
2、使用连接(JOIN)来代替子查询
3、适用联合(UNION)来代替手动创建的临时表
4、事务处理
5、锁定表、优化事务处理
6、适用外键,优化锁定表
7、建立索引
8、优化查询语句
Q:SQL 注入漏洞产生的原因?如何防止?
SQL 注入产生的原因:程序开发过程中不注意规范书写 sql 语句和对特殊字符进行过滤,导致客户端可以通过全局变量 POST 和 GET 提交一些 sql 语句正常执行。
防止 SQL 注入的方式:
- 开启配置文件中的 magic_quotes_gpc 和 magic_quotes_runtime 设置
- 执行 sql 语句时使用 addslashes 进行 sql 语句转换
- Sql 语句书写尽量不要省略双引号和单引号。
- 过滤掉 sql 语句中的一些关键词:update、insert、delete、select、 * 。
- 提高数据库表和字段的命名技巧,对一些重要的字段根据程序的特点命名,取不易被猜到的。
Q:为表中得字段选择合适得数据类型
字段类型优先级: 整形>date,time>enum,char>varchar>blob,text
优先考虑数字类型,其次是日期或者二进制类型,最后是字符串类型,同级别的数据类型,应该优先选择占用空间小的数据类型
Q:对于关系型数据库而言,索引是相当重要的概念,请回答有关索引的几个问题:
1、索引的目的是什么?
- 快速访问数据表中的特定信息,提高检索速度
- 创建唯一性索引,保证数据库表中每一行数据的唯一性。
- 加速表和表之间的连接
- 使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间
2、索引对数据库系统的负面影响是什么?
负面影响:
创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加;索引需
要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间;
当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速
度。
3、为数据表建立索引的原则有哪些?
-
在最频繁使用的、用以缩小查询范围的字段上建立索引。
-
在频繁使用的、需要排序的字段上建立索引
-
全文本索引(Full-Text Index)是一种用于在文本数据中进行全文本搜索的索引技术,它可以提高文本数据的搜索效率和准确性。在 MySQL 中,可以通过在表中建立全文本索引来实现对文本数据的高效搜索。
具体来说,全文本索引会对指定的文本字段进行分词处理,并将分词结果存储在索引结构中。在进行搜索时,用户输入的搜索关键词也会进行分词处理,并在索引结构中查找包含这些关键词的记录。由于使用了分词技术,全文本索引可以实现模糊匹配、近义词匹配等功能,从而提高搜索的准确性和灵活性。
需要注意的是,全文本索引只能用于 MyISAM 和 InnoDB 存储引擎,而且只能对 CHAR、VARCHAR、TEXT 和 BLOB 类型的字段建立索引。此外,全文本索引的建立和使用需要一定的成本,包括索引占用的存储空间、索引维护的性能开销等。因此,在使用全文本索引时需要权衡其带来的性能提升和成本开销。
4、什么情况下不宜建立索引?
对于查询中很少涉及的列或者重复值比较多的列,不宜建立索引。
对于一些特殊的数据类型,不宜建立索引,比如文本字段(text)等
除了文本字段(text)外,还有以下数据类型不宜建立索引:
- BLOB 和 LONGBLOB:这些类型用于存储二进制数据或大量的文本数据,通常不适合建立索引。
- ENUM 和 SET:这些类型用于存储枚举值或多选值,通常只有很少的取值,因此不需要建立索引。
- JSON:这个类型用于存储 JSON 格式的数据,由于 JSON 数据结构的复杂性,建立索引对性能的提升有限。
需要注意的是,在某些情况下,即使是以上类型的字段也可能需要建立索引,例如在特定的查询场景下。此外,对于一些较大的文本字段,可以使用全文本索引(Full-Text Index)来提高查询性能。
Q:MySQL 外连接、内连接与自连接的区别
交叉连接
交叉连接(Cross Join),也称为笛卡尔积连接(Cartesian Join),是一种基于两个表的笛卡尔积进行的连接操作。具体来说,交叉连接会返回两个表中所有可能的组合,即将第一个表的每一行都与第二个表的每一行进行组合,从而得到一个新表。新表的行数等于第一个表的行数乘以第二个表的行数。
交叉连接通常用于需要生成所有可能组合的场景,例如在进行数据分析或数据挖掘时,需要对多个数据集进行组合分析。不过,由于交叉连接会生成大量的结果集,因此在实际应用中需要谨慎使用,以避免出现性能问题和不必要的数据冗余。在实际应用中,通常会使用其他类型的连接操作(如内连接、外连接等)来实现数据的关联查询。
可以使用 CROSS JOIN 关键字来实现,
SELECT *
FROM table1
CROSS JOIN
table2
WHERE table1.id = table2.id;
内连接
在 SQL 中,内连接(Inner Join)可以使用 INNER JOIN 或 JOIN 关键字来实现。内连接用于基于两个表的共同列进行匹配,并返回满足连接条件的行的组合。INNER JOIN 和 JOIN 是等价的,它们可以互换使用。例如:
SELECT *
FROM table1
INNER JOIN
table2
ON table1.id = table2.id;
左外连接,右外连接
在 SQL 中,外连接(Outer Join)用于返回两个表中的所有行,并根据连接条件匹配行,如果没有匹配的行,则会返回 NULL 值。外连接可以分为左外连接、右外连接和全外连接三种类型,分别对应返回左表或右表的所有行、返回两个表的所有行以及返回左表和右表的所有行。
左外连接(Left Outer Join)用于返回左表的所有行以及满足连接条件的右表的行,如果没有匹配的右表的行,则返回 NULL 值。左外连接可以使用 LEFT JOIN 或 LEFT OUTER JOIN 关键字来实现。例如:
SELECT *
FROM table1
LEFT JOIN
table2
ON table1.id = table2.id;
全外连接
MySQL 暂不支持全外连接(Full Outer Join),它没有提供专门的 FULL JOIN 或 FULL OUTER JOIN 关键字来实现全外连接,而是需要使用 UNION 和 LEFT JOIN 或 RIGHT JOIN 来模拟实现。
具体来说,可以通过使用 UNION 和 LEFT JOIN 或 RIGHT JOIN 来实现全外连接。例如:
SELECT *
FROM table1
LEFT JOIN table2
ON table1.id = table2.id
UNION
SELECT *
FROM table1
RIGHT JOIN table2
ON table1.id = table2.id
WHERE table1.id IS NULL;
Q:SQL 语言包括哪几部分?每部分都有哪些操作关键字
四部分:
SQL(Structured Query Language)语言是一种用于管理关系型数据库的标准化语言,它包括四个主要部分:数据定义(DDL)、数据操纵(DML)、数据控制(DCL)和数据查询(DQL)。
- 数据定义语言(DDL):DDL 用于定义数据库的结构,包括创建、修改和删除数据库、表、视图、索引、存储过程、触发器等对象。常见的 DDL 命令包括 CREATE、ALTER 和 DROP 等命令。例如,CREATE TABLE 用于创建一个新的表,ALTER TABLE 用于修改一个表的结构,DROP TABLE 用于删除一个表。
- 数据操纵语言(DML):DML 用于操作数据库中的数据,包括插入、更新和删除数据。常见的 DML 命令包括 SELECT、INSERT、UPDATE 和 DELETE 等命令。例如,SELECT 用于查询数据库中的数据,INSERT 用于插入新的数据,UPDATE 用于更新现有的数据,DELETE 用于删除现有的数据。
- 数据控制语言(DCL):DCL 用于管理数据库的安全性和权限控制,包括创建、修改和删除用户、授予和撤销用户的权限等。常见的 DCL 命令包括 GRANT 和 REVOKE 等命令。例如,GRANT 用于授予用户访问数据库的权限,REVOKE 用于撤销用户的权限。
- 数据查询语言(DQL):DQL 用于查询数据库中的数据,是 SQL 语言中最常用的部分。常见的 DQL 命令包括 SELECT、FROM、WHERE、GROUP BY、HAVING 和 ORDER BY 等命令。例如,SELECT 用于选择要查询的列,FROM 用于指定要查询的表,WHERE 用于指定查询条件,GROUP BY 用于分组汇总数据,HAVING 用于过滤分组后的数据,ORDER BY 用于排序查询结果。
关键字:
- 数据定义:Create Table,Alter Table,Drop Table, Craete/Drop Index 等
- 数据操纵:Select ,insert,update,delete,
- 数据控制:grant,revoke
- 数据查询:select
Q:关系型数据库完整性约束
完整性约束是指对数据库中数据的合法性进行限制和保护的规则,它可以保证数据库中数据的正确性和一致性。常见的完整性约束包括以下几种:
- 实体完整性约束:保证每个表中的每行数据都有一个唯一的标识符,即主键。主键不能为 NULL,且必须在表中唯一。
- 参照完整性约束:保证表之间的关系的正确性。在一个表中的外键必须引用另一个表中已经存在的主键。如果在另一个表中没有对应的主键,则无法插入外键。
- 唯一性约束:保证某个列或一组列的值在表中唯一。唯一性约束可以用于任何列,不一定是主键列。
- 检查约束:保证某个列或一组列的值符合特定的条件。例如,可以使用检查约束限制某个列的取值范围或格式。
- 默认值约束:为某个列指定默认值。如果在插入数据时没有指定该列的值,则使用默认值。
通过使用完整性约束,可以保证数据库中数据的正确性和一致性,减少数据出错的可能性,提高数据的质量和可靠性。
与表有关的约束:包括列约束(NOT NULL(非空约束))和表约束(PRIMARY KEY、foreign key、check、UNIQUE)
Q:数据库三范式
在数据库三范式中,属性值指的是一个关系模式中的某个属性对应的具体值(其实就是table的cell)。例如,如果一个关系模式 R 包含属性 A、B、C,其中 A 是主键,那么一个关系实例可以表示为以下形式的表格:
A | B | C |
---|---|---|
1 | x | y |
2 | a | b |
3 | p | q1,q2,q3 |
第一范式:1NF
是对属性的原子性约束,要求属性具有原子性,不可再分解 ,不能包含多个值或多个数据项
第三行第三列的单元格中的值是 q1,q2,q3
很明显,其包含多个可拆分的值,不满足 1NF
第二范式:2NF
是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
在这个表格中,每个单元格中的值都是一个属性值。例如,第一行第一列的单元格中的值是 1,它是属性 A 的一个属性值,满足2NF
;
第三范式:3NF
依赖关系不可传递
具体来说,如果一个关系模式 R 满足以下条件,就可以称之为符合
R 中的每个属性都是原子的,不可再分。
R 中的每个非主属性都完全依赖于其所有的主属性,而不是仅依赖于某个主属性的一部分。
R 中不存在传递依赖关系,即如果 A→B,B→C,则不能存在 A→C 的依赖关系。
Q:范式化设计优缺点:
优点:
在数据库设计中,三范式的目的是为了消除数据冗余和数据不一致,提高数据的可靠性和一致性,同时也可以提高数据库的性能和可维护性。因此,在遵循三范式的规范下,每个属性都必须是原子性的,不能包含多个值或多个数据项,以保证数据的正确性和一致性,因为不会存在数据冗余使得更新快,体积小,更新和维护数据时也更加方便和高效。
缺点:
对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化
Q:反范式化的优缺点
- 优点:可以减少表得关联,可以更好得进行索引优化
- 缺点:数据冗余以及数据异常,数据得修改需要更多的成本
Q:说说视图的优点
- 视图能够简化用户的操作
- 视图使用户能以多种角度看待同一数据;
- 视图为数据库提供了一定程度的逻辑独立性;
- 视图能够对机密数据提供安全保护。
Q:主键、外键和索引的区别?
主键、外键和索引是关系型数据库中常用的三个概念,它们的作用和含义如下:
- 主键(Primary Key):主键是用于唯一标识关系表中每一条记录的一列或一组列。主键必须是唯一的,且不能为 NULL。主键的作用是保证表中每条记录的唯一性,便于数据的查找和修改。
- 外键(Foreign Key):外键是用于建立两个关系表之间关联的一列或一组列。外键指向另一个表的主键,用于建立表与表之间的关联关系。外键的作用是保证表与表之间的数据一致性和完整性,避免数据的冗余和不一致。
- 索引(Index):索引是用于加速数据库查询操作的一种数据结构。索引可以建立在表的一列或多列上,用于快速查找表中符合特定条件的数据。索引的作用是提高查询效率,减少查询时间。
总的来说,主键、外键和索引都是用于优化数据库性能和保证数据一致性的重要手段。
作用
- 主键用于唯一标识每条记录,
- 外键用于建立表与表之间的关联关系,
- 索引用于提高查询效率。
个数:
- 主键–主键只能有一个
- 外键–一个表可以有多个外键
- 索引–一个表可以有多个唯一索引
Q:你可以用什么来确保表格里的字段只接受特定范围里的值?
Check 约束,它在数据库表格里被定义,用来限制输入该列的值。
触发器也可以被用来限制数据库表格里的字段能够接受的值,但是这种办法要求
触发器在表格里被定义,这可能会在某些情况下影响到性能。
Q:说说对 SQL 语句优化有哪些方法?(选择几条)
1、Where 子句中:where 表之间的连接必须写在其他 Where 条件之前,那些可
以过滤掉最大数量记录的条件必须写在 Where 子句的末尾.HAVING 最后。
2、用 EXISTS 替代 IN、用 NOT EXISTS 替代 NOT IN。
3、 避免在索引列上使用计算
4、避免在索引列上使用 IS NULL 和 IS NOT NULL
5、对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉
及的列上建立索引。
6、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃
使用索引而进行全表扫描
7、应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用
索引而进行全表扫描
Java
Q:面向对象的特征有哪些方面
抽象:
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
继承:
继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java 与模式》或《设计模式精解》中关于桥梁模式的部分)。
封装:
通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是
封装得足够好的,因为几个按键就搞定了所有的事情)。
多态性:
多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。
如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:
当 A 系统访问 B 系统提供的服务时,B系统有多种提供服务的方式,但一切对 A 系统来说都是透明的(就像电动剃须刀是 A 系统,它的供电系统是 B 系统,B 系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A 系统只会通过 B 类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。
方法重载
(overload)实现的是编译时的多态性(也称为前绑定),
方法重写
(override)实现的是运行时的多态性(也称为后绑定),运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:
- 方法重写(子类继承父类并重写父类中已有的或抽象的方法);
- 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)
Q:Java 基本类型(primitive type)
在 Java 中,基本类型(primitive type)是指一组预定义的数据类型,它们是语言的基础构建块,直接支持程序的基本操作。Java 中的基本类型包括以下 8 种:
- boolean:布尔类型,表示 true 或 false。
- byte:字节类型,表示 8 位有符号整数。
- short:短整数类型,表示 16 位有符号整数。
- int:整数类型,表示 32 位有符号整数。
- long:长整数类型,表示 64 位有符号整数。
- float:单精度浮点数类型,表示 32 位浮点数。
- double:双精度浮点数类型,表示 64 位浮点数。
- char:字符类型,表示单个字符,使用 Unicode 编码。
- 基本类型是直接存储在栈内存中的,具有很快的访问速度和较小的存储空间,适合于存储简单的数据类型和数值计算。在 Java 中,基本类型的值是不可变的,也就是说,一旦定义了一个基本类型的变量,就不能改变它的值。
- 除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型
Q:float f=3.4;是否正确?
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换
float f =(float)3.4;
//或者
float f =3.4F;
Q:short s1 = 1; s1 = s1 + 1;
有错吗?short s1 = 1; s1 += 1;
有错吗 ?
对于 short s1 = 1; s1 = s1 + 1;
错误
由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1;
正确
因为 s1+= 1;
相当于 s1 = (short)(s1 + 1);
其中有隐含的强制类型转换。
Q:int 和 Integer 有什么区别?
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
- 原始类型: boolean,char,byte,short,int,long,float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
class AutoUnboxingTest {
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将 3 自动装箱成 Integer 类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a 自动拆箱成 int 类型再和 c 比较
}
}
Q:还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:
public class Test03 {
public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
}
如果不明就里很容易认为两个输出要么都是 true 要么都是 false。首先需要注意的是 f1、f2、f3、f4 四个变量都是 Integer 对象引用,所以下面的==运算比较的不
是值而是引用。装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如果看看 valueOf 的源代码就知道发生了什么。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache 是 Integer 的内部类,其代码如下所示:
/**
* Cache to support the object identity semantics of autoboxing for
values between
* -128 and 127 (inclusive) as required by JLS.
* *
The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>}
option.
* During VM initialization, java.lang.Integer.IntegerCache.high
property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int,
ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer对象,而是直接引用常量池中的 Integer 对象,所以上面的面试题中 System.out.println(f3 == f4); 的结果是 false。
- Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型;
- Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
Q:用最有效率的方法计算 2 乘以 8
2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)
在计算机中,符号 << 表示左移运算符,将一个二进制数向左移动指定的位数,相当于在该数的二进制表示中添加零。例如,2 的二进制表示为 10,将其左移 3 位,得到 10000,即二进制的 16。因此,2 << 3 的结果是 16。
Q:数组有没有 length()方法?String 有没有 length()方法?
-
Java数组没有 length()方法,有 length 的属性。String 有 length()方法。
-
JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆
Q:在 Java 中,如何跳出当前的多重嵌套循环?
在 Java 中,可以使用带有标签的 break 语句来跳出多重嵌套循环。标签是一个紧跟着冒号的标识符,用于标识循环语句。在 break 语句中,指定标签将退出带有该标签的循环。
例如,以下代码演示了如何在多重嵌套循环中使用标签和 break 语句:
copy codeouterloop:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (i * j > 50) {
System.out.println("Breaking");
break outerloop;
}
}
}
在上述代码中,outerloop 是标签,它标识了外部循环。当 i * j 大于 50 时,break outerloop 语句将跳出带有标签 outerloop 的循环。
作用有点类似于 C 和 C++中的 goto 语句,但是就像要避免使用 goto 一样,应该避免使用带标签的 break 和 continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好)
使用带标签的 break 和 continue 可能会导致代码变得难以理解和维护,从而降低代码的可读性和可维护性,这可能是使用它们的一个缺点。
- 增加复杂性:使用带标签的 break 和 continue 可能会增加代码的复杂性,使其更难以理解和调试。
- 破坏结构:使用带标签的 break 和 continue 可能会破坏代码的结构,使其更难以维护和修改。
- 难以重构:使用带标签的 break 和 continue 可能会使代码难以重构,因为它们可能会使代码中的控制流变得不清晰,从而使重构更加困难。
- 增加错误的可能性:使用带标签的 break 和 continue 可能会增加错误的可能性,因为它们可能会导致控制流错误,从而导致意外的行为和错误。
因此,虽然使用带标签的 break 和 continue 可能有时可以使代码更简洁和高效,但在大多数情况下,应该避免使用它们,以提高代码的可读性和可维护性。
Q:构造器(constructor)是否可被重写(override)?
构造器不能被继承,因此不能被重写,但可以被重载。
Q:是否可以继承 String 类?
String 类是 final 类,不可以被继承。
补充:继承 String 本身就是一个错误的行为,对 String 类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)
Q:String 和 StringBuilder、StringBuffer 的区别?
- Java 平台提供了两种类型的字符串:String 和 StringBuffer/StringBuilder,它们可以储存和操作字符串。
- String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。
- StringBuffer/StringBuilder 类表示的字符串对象可以直接进行修改。
StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized 修饰,因此它的效率也比 StringBuffer 要高
- String 对象的 intern 方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与 String 对象的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
- 字符串的+操作其本质是创建了 StringBuilder 对象进行 append 操作,然后将拼接后的 StringBuilder 对象用toString 方法处理成 String 对象,这一点可以用 javap -c StringEqualTest.class命令获得 class 文件对应的 JVM 字节码指令就可以看出来
Q:重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
- 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
Q:华为的面试题中曾经问过这样一个问题 - “为什么不能根据返回类型来区分重载”,快说出你的答案吧!
在 Java 中,方法的重载(Overload)是指在一个类中定义多个方法,它们具有相同的名称但不同的参数列表。在调用方法时,编译器会根据传递给方法的参数列表来确定调用的具体方法。
客观来说:
- 返回类型不足以区分方法:在 Java 中,方法的返回类型通常不足以区分方法,因为方法的返回类型可以是任何类型,包括基本数据类型、对象类型、数组类型等。因此,如果根据返回类型来区分重载,可能会导致多个方法具有相同的返回类型,无法确定调用哪一个方法。
- 编译器无法确定调用哪个方法:如果根据返回类型来区分重载,编译器无法确定调用哪个方法,因为它无法根据传递给方法的参数列表来推断出具体的返回类型。这可能会导致编译错误或运行时错误。
因此,在 Java 中,方法的重载只能根据方法的参数列表来区分,而不能根据返回类型来区分。如果需要定义具有相同名称但不同返回类型的方法,应该使用不同的方法名来避免重载冲突。
主观来说:
- 代码可读性差:如果根据返回类型来区分重载,可能会导致多个方法具有相同的名称和参数列表,但返回值不同,这会使代码难以理解和维护。
- 违反方法重载的原则:方法重载的原则是方法名称相同,参数列表不同,而不是返回类型不同。如果根据返回类型来区分重载,就会违反这个原则,使代码变得混乱和不规范。
- 运行时性能影响:在 Java 中,方法的调用是通过虚拟机动态绑定实现的。如果根据返回类型来区分重载,可能会导致虚拟机在运行时需要进行更多的类型检查和转换操作,从而降低程序的性能。
因此,根据返回类型来区分重载不仅不符合 Java 的语法规则,而且还会导致代码的可读性差、违反方法重载的原则和运行时性能影响。因此,开发人员应该遵循 Java 的方法重载规则,使用不同的参数列表来区分重载。
Q:char 型变量中能不能存贮一个中文汉字,为什么?
char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一
个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。
补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统
中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader 和 OutputStreamReader,
这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于 C 程序员来说,要完成这样的编码转换恐怕要依赖于 union(联合体/共用体)共享内
存的特征来实现了。
Q:抽象类(abstract class)和接口(interface)有什么异同?
相同点:
- 抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。
- 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
不同点:
- 接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。
- 抽象类中的成员可以是 private、默认、protected、public 的,而接口中的成员全都是 public 的。
- 抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。
- 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
Q:静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
Static Nested Class 是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化。其语法看起
来挺诡异的,如下所示。
/**
* 扑克类(一副扑克)
*
* @author 骆昊
*/
public class Poker {
private static String[] suites = { "黑桃", "红桃", "草花", "方块" };
private static int[] faces = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
private Card[] cards;
/**
* 构造器
*
*/
public Poker() {
cards = new Card[52];
for (int i = 0; i < suites.length; i++) {
for (int j = 0; j < faces.length; j++) {
cards[i * 13 + j] = new Card(suites[i], faces[j]);
}
}
}
/**
* 洗牌 (随机乱序)
*
*/
public void shuffle() {
for (int i = 0, len = cards.length; i < len; i++) {
int index = (int) (Math.random() * len);
Card temp = cards[index];
cards[index] = cards[i];
cards[i] = temp;
}
}
/**
* 发牌
*
* @param index 发牌的位置
*
*/
public Card deal(int index) {
return cards[index];
}
/**
* 卡片类(一张扑克)
* [内部类]
*
* @author 骆昊
*
*/
public class Card {
private String suite; // 花色
private int face; // 点数
public Card(String suite, int face) {
this.suite = suite;
this.face = face;
}
@Override
public String toString() {
String faceStr = "";
switch (face) {
case 1:
faceStr = "A";
break;
case 11:
faceStr = "J";
break;
case 12:
faceStr = "Q";
break;
case 13:
faceStr = "K";
break;
default:
faceStr = String.valueOf(face);
}
return suite + faceStr;
}
}
}
测试代码:
class PokerTest {
public static void main(String[] args) {
Poker poker = new Poker();
poker.shuffle(); // 洗牌
Poker.Card c1 = poker.deal(0); // 发第一张牌
// 对于非静态内部类 Card
// 只有通过其外部类 Poker 对象才能创建 Card 对象
Poker.Card c2 = poker.new Card("红心", 1); // 自己创建一张牌
System.out.println(c1); // 洗牌后的第一张
System.out.println(c2); // 打印: 红心 A
}
}
Q: 下面的代码哪些地方会产生编译错误?
class Outer {
class Inner {
}
public static void foo() {
new Inner();
}
public void bar() {
new Inner();
}
public static void main(String[] args) {
new Inner();
}
}
注意 :Java 中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中 foo和 main 方法都是静态方法 ,静态方法中没有 this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做:
new Outer().new Inner();
Q:Java 中会存在内存泄漏吗,请简单描述。
理论上 Java 因为有垃圾回收机制( GC)不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无
用但可达的对象,这些对象不能被 GC 回收 ,因此也会导致内存泄露的发生 。
在 Java 中,无用但可达的对象是指已经不再被程序使用,但是仍然存在于内存中,并且可以通过某些引用路径(如强引用、软引用、弱引用、虚引用等)到达。这种对象占用了内存资源,可能会导致内存泄漏和程序性能问题。
例如,以下是一个创建无用但可达对象的示例:
copy codepublic class MyClass {
private static List<Object> list = new ArrayList<>();
public void add(Object obj) {
list.add(obj);
}
public static void main(String[] args) {
MyClass myClass = new MyClass();
Object obj = new Object();
myClass.add(obj);
obj = null; // obj 变量不再引用对象
System.gc(); // 手动触发垃圾回收
}
}
在这个示例中,MyClass
类中的 add()
方法将一个对象添加到静态的 list
集合中,这个对象被引用了一次。在 main()
方法中,创建了一个 MyClass
对象和一个 Object
对象,并将 Object
对象添加到 MyClass
对象中。然后,将 Object
对象的引用变量 obj
设为 null
,这意味着这个对象不再被引用。最后,手动触发垃圾回收。但是,由于 Object
对象仍然存在于 list
集合中,因此它仍然是可达的,无法被垃圾回收器回收。
为了避免无用但可达的对象,开发人员应该注意以下几点:
- 及时释放对象引用:在程序中,如果一个对象不再被使用,应该尽早将其引用设为
null
,以便垃圾回收器可以回收它。 - 避免创建不必要的对象:在程序中,应该避免创建不必要的对象,尤其是在循环或递归等代码块中。如果必须创建对象,可以使用对象池等技术来重复利用对象,减少垃圾回收的负担。
- 使用弱引用等技术:在程序中,可以使用弱引用、软引用、虚引用等技术来管理对象的生命周期,避免出现无用但可达的对象。
例 如Hibernate 的 Session( 一级缓存 )中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象 ,如果不及时关闭(close)或清空( flush)一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在
内存泄露的问题,当我们用 pop 方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过
期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发 Disk Paging(物理内存与硬盘的虚拟内存交换数据),甚至造成 OutOfMemoryError。
Q:抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被 synchronized修饰?
都不能。
- 抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
- 本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
- synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
Q:阐述静态变量和实例变量的区别
静态变量是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷
贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。补充:在 Java 开发中,上下文类和工具类中通常会有大量的静态成员。
Q:是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。
Q:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调
用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和 C#中可以通过传引用或传输出参数来改变传入的参数的值。在 C#中可以用ref
编写如下所示的代码,但是在 Java 中却做不到
using System;
namespace CS01
{
class Program
{
public static void swap(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
public static void Main(string[] args)
{
int a = 5,
b = 10;
swap(ref a, ref b);
// a = 10, b = 5;
Console.WriteLine("a = {0}, b = {1}", a, b);
}
}
}
说明:Java 中没有传引用实在是非常的不方便,这一点在 Java 8 中仍然没有得到改进,正是如此在 Java 编写的代码中才会出现大量的 Wrapper 类(将需要通过
方法调用修改的引用置于一个 Wrapper 类中,再将 Wrapper 对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从 C 和 C++转型为 Java 程序员的开发者无法容忍
Q:如何实现对象克隆
有两种方式:
- 实现 Cloneable 接口并重写 Object 类中的 clone()方法;
- 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
Q:GC 是什么?为什么要有 GC?
GC 是垃圾收集的意思 ,内存处理是编程人员容易出现问题的地方 ,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃, Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。Java 程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但 JVM 可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死
亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在 Java 诞生初期,垃圾回收是 Java
最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今 Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常
觉得 iOS 的系统比 Android 系统有更好的用户体验,其中一个深层次的原因就在于 Android 系统中垃圾回收的不可预知性。
补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的 Java 进程既有栈又有堆。栈保存了原始型局部变量,堆
保存了要创建的对象。Java 平台对堆内存回收和再利用的基本算法被称为标记和清除,但是 Java 对其进行了改进,采用“分代式垃圾收集”。这种方法会跟 Java
对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
-
伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
-
幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
-
终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。
与垃圾回收相关的 JVM 参数:
- -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
- -Xmn — 堆中年轻代的大小
- -XX:-DisableExplicitGC — 让 System.gc()不产生任何作用
- -XX:+PrintGCDetails — 打印 GC 的细节
- -XX:+PrintGCDateStamps — 打印 GC 操作的时间戳
- -XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
- -XX:NewRatio — 可以设置老生代和新生代的比例
- -XX:PrintTenuringDistribution — 设置每次新生代 GC 后输出幸存者乐园中对象年龄的分布
- -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
- -XX:TargetSurvivorRatio:设置幸存区的目标使用率
Q:String s = new String(“xyz”);创建了几个字符串对象?
两个对象,一个是静态区的 ” xyz” ,一个是用 new 创建在堆上的对象。
Q:接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concreteclass)?
接口可以继承接口,而且支持多重继承。抽象类可以实现 (implements)接口,抽象类可继承具体类也可以继承抽象类
Q:一个”.java”源文件中是否可以包含多个类(不是内部类)?有什么限制?
可以,但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。
Q:Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来实现事件监听和回调。
Q:内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员,无限制。
Q:Java 中的 final 关键字有哪些用法?
- 修饰类:表示该类不能被继承;
- 修饰方法:表示方法不能被重写;
- 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
Q:指出下面程序的运行结果
class A {
static {
System.out.print("1");
}
public A() {
System.out.print("2");
}
}
class B extends A {
static {
System.out.print("a");
}
public B() {
System.out.print("b");
}
}
public class Hello {
public static void main(String[] args) {
A ab = new B();
ab = new B();
}
}
执行结果:1a2b2b。
创建对象时构造器的调用顺序是:
- 先初始化静态成员,
- 然后调用父类构造器,
- 再初始化非静态成员,
- 最后调用自身构造器。
Q:数据类型之间的转换:如何将字符串转换为基本数据类型? 如何将基本数据类型转换为字符串?
- 调用基本数据类型对应的包装类中的方法 parseXXX(String)或valueOf(String)即可返回相应基本类型;
- 一种方法是将基本数据类型与空字符串(”“)连接(+)即可获得其所对应的字符串;另一种方法是调用 String 类中的 valueOf()方法返回相应字符串
Q:如何实现字符串的反转及替换?
方法很多,可以自己写实现也可以使用 String 或 StringBuffer/StringBuilder 中的方法。有一道很常见的面试题是用递归实现字符串反转,代码如下所示:
public static String reverse(String originStr) {
if(originStr == null || originStr.length() <= 1)
return originStr;
return reverse(originStr.substring(1)) + originStr.charAt(0);
}
Q:怎样将 GB2312 编码的字符串转换为 ISO-8859-1 编码的字符串?
String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");
Q:日期和时间:
- 如何取得年月日、小时分钟秒?
- 如何取得从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的毫秒数?
- 如何取得某月的最后一天?
- 如何格式化日期?
问题 1:创建 java.util.Calendar 实例,调用其 get()方法传入不同的参数即可获得参数所对应的值。Java 8 中可以使用 java.time.LocalDateTimel 来获取,代码如下所示:
public class DateTimeTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
System.out.println(cal.get(Calendar.YEAR));
System.out.println(cal.get(Calendar.MONTH)); // 0 - 11
System.out.println(cal.get(Calendar.DATE));
System.out.println(cal.get(Calendar.HOUR_OF_DAY));
System.out.println(cal.get(Calendar.MINUTE));
System.out.println(cal.get(Calendar.SECOND));
// Java 8
LocalDateTime dt = LocalDateTime.now();
System.out.println(dt.getYear());
System.out.println(dt.getMonthValue()); // 1 - 12
System.out.println(dt.getDayOfMonth());
System.out.println(dt.getHour());
System.out.println(dt.getMinute());
System.out.println(dt.getSecond());
}
}
问题 2:以下方法均可获得该毫秒数。
Calendar.getInstance().getTimeInMillis();
System.currentTimeMillis();
Clock.systemDefaultZone().millis(); // Java 8
问题 3:代码如下所示 :
Calendar time = Calendar.getInstance();
time.getActualMaximum(Calendar.DAY_OF_MONTH);
问题 4:
利用 java.text.DataFormat 的子类(如 SimpleDateFormat 类)中的format(Date)方法可将日期格式化。Java 8 中可以用java.time.format.DateTimeFormatter 来格式化时间日期,代码如下所示 :
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
class DateFormatTest {
public static void main(String[] args) {
SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
Date date1 = new Date();
System.out.println(oldFormatter.format(date1));
// Java 8
DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date2 = LocalDate.now();
System.out.println(date2.format(newFormatter));
}
}
补充:Java 的时间日期 API 一直以来都是被诟病的东西,为了解决这一问题,Java8 中引入了新的时间日期 API,其中包括 LocalDate、LocalTime、LocalDateTime、Clock、Instant 等类,这些的类的设计都使用了不变模式,因此是线程安全的设计。如果不理解这些内容,可以参考大佬 骆昊
Q:打印昨天的当前时刻
import java.util.Calendar;
class YesterdayCurrent {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1);
System.out.println(cal.getTime());
}
}
在 Java 8 中,可以用下面的代码实现相同的功能
import java.time.LocalDateTime;
class YesterdayCurrent {
public static void main(String[] args) {
LocalDateTime today = LocalDateTime.now();
LocalDateTime yesterday = today.minusDays(1);
System.out.println(yesterday);
}
}
Q:比较一下 Java 和 JavaSciprt
JavaScript 与 Java 是两个公司开发的不同的两个产品。
Java 是原 SunMicrosystems 公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;
而 JavaScript 最早是 Netscape 公司的产品,为了扩展 Netscape 浏览器的功能而开发的一种可以嵌入 Web 页面中运行的基于对象和事件驱动的解释性语言。
JavaScript 的前身是 LiveScript;而 Java 的前身是 Oak 语言。
下面对两种语言间的异同作如下比较:
- 基于对象和面向对象:Java 是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript 是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
- 解释和编译:Java 的源代码在执行之前,必须经过编译。JavaScript 是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了 JIT(即时编译)技术来提升 JavaScript 的运行效率)
- 强类型变量和类型弱变量:Java 采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript 中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript 的解释器在运行时检查推断其数据类型。
- 代码格式不一样。
补充:上面列出的四点是网上流传的所谓的标准答案。其实 Java 和 JavaScript最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势
是函数式语言和动态语言。在 Java 中类(class)是一等公民,而 JavaScript 中函数(function)是一等公民,因此 JavaScript 支持函数式编程,可以使用 Lambda函数和闭包(closure),当然 Java 8 也开始支持函数式编程,提供了对 Lambda表达式以及函数式接口的支持。对于这类问题,在面试的时候最好还是用自己的语言回答会更加靠谱,不要背网上所谓的标准答案
Q:什么时候用断言(assert)?
断言在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试
时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true;如果表
达式的值为 false,那么系统会报告一个 AssertionError。断言的使用如下面的代码所示:
assert(a > 0); // throws an AssertionError if a <= 0
断言可以有两种形式:
assert Expression1;
Expression1 应该总是产生一个布尔值。assert Expression1 : Expression2 ;
Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。
要在运行时启用断言,可以在启动 JVM 时使用-enableassertions 或者-ea 标记。
要在运行时选择禁用断言,可以在启动 JVM 时使用-da 或者-disableassertions标记。
要在系统类中启用或禁用断言,可使用-esa 或-dsa 标记。还可以在包的基础上启用或者禁用断言。
注意:断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。
断言在Java中用于检查代码中的预期条件是否成立。这些预期条件通常是不变量或其他已知的先决条件。因此,断言的主要目的是帮助开发人员快速发现和调试代码中的错误。
不变量是指在程序执行期间始终保持不变的条件。例如,一个类的某个属性始终保持非空,或者一个循环变量始终保持在某个范围内。使用断言来检查这些不变量可以帮助开发人员捕获可能导致程序错误的条件。
与异常处理不同,断言不应该用于处理异常情况。异常处理是指在程序执行期间发生错误时采取的措施。例如,如果打开文件失败,程序可能会抛出一个IOException。在这种情况下,程序应该有一种方法来处理异常情况,例如记录错误信息或提供替代方案。
相比之下,断言是用于检查预期条件是否成立。如果条件不成立,程序应该停止执行,并给出错误信息。断言并不是用于处理异常情况的,而是用于帮助开发人员在开发和测试期间快速发现和修复代码中的错误。
因此,断言应该仅用于检查不变量和其他预期条件,而不应该用于处理异常情况。
Q:Error 和 Exception 有什么区别?
- Error表示系统级错误和程序不必处理的异常的一种类型。它通常表示一些严重的问题,例如虚拟机错误、内存不足、栈溢出等,这些问题可能导致程序崩溃或无法继续执行。尽管在某些情况下可能有恢复的可能,但这通常是很困难的。因此,处理Error异常的最佳方式是让程序崩溃并记录错误信息,以便进行后续调查和修复。;
- Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。
面试题:2005 年摩托罗拉的面试中曾经问过这么一个问题“If a process reportsa stack overflow run-time error, what’s the most possible cause?”,给了四个选项
- a. lack of memory(内存不足);
- b. write on an invalid memory space(写入无效的内存空间);
- c.recursive function calling(递归函数调用);
- d. array index out of boundary(数组索引越界).
Java 程序在运行时也可能会遭遇 StackOverflowError,这是一个无法恢复的错误,只能重新修改代码了,这个面试题的答案是 c。如果写了不能迅速收敛的递归,则很有可能引发栈溢出的错误,如下所示 :
class StackOverflowErrorTest {
public static void main(String[] args) {
main(null);
}
}
提示:用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件(什么时候就不再继续递归)。
Q:try{}里有一个 return 语句,那么紧跟在这个 try 后的finally{}里的代码会不会被执行,什么时候被执行,在 return前还是后?
会执行,在方法返回调用者前执行。
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完
毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中
直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误,Eclipse 中可以进行设置,强烈建议将此项设置为编译错误。
Q:Java 语言如何进行异常处理,关键字:throws、throw、try、catch、finally 分别如何使用?
Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类
的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。
一般情况下是用 try 来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;
try 用来指定一块预防所有异常的程序;catch 子句紧跟在 try 块后面,用来指定你想要捕获的异常的类型;throw 语句用来明确地抛出一个异常;
throws 用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟);
finally 为确保一段代码不管发生什么异常状况都要被执行;try 语句可以嵌套,每当遇到一个 try 语句,异常的结构就会被放入异常栈中,直到所有的 try 语句都完成。如果下一级的try 语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的 try 语句或者最终将异常抛给 JVM。
Q:列出一些你常见的运行时异常?
- ArithmeticException(算术异常)
- ClassCastException (类转换异常)
- IllegalArgumentException (非法参数异常)
- IndexOutOfBoundsException (下标越界异常)
- NullPointerException (空指针异常)
- SecurityException (安全异常)
Q:阐述 final、finally、finalize 的区别。
- final:修饰符(关键字)有三种用法:如果一个类被声明为 final,意味着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。将变量声明为 final,可以保证它们在使用中不被改变,被声明为 final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为 final 的方法也同样只能使用,不能在子类中被重写。
- finally:通常放在 try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以将释放外部资源的代码写在 finally 块中。
- finalize:Object 类中定义的方法,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写 finalize()方法可以整理系统资源或者执行其他清理工作。
Q:里氏转换原则
里氏代换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一个重要原则,由Barbara Liskov于1987年提出。
该原则指出,一个基类的对象可以被其子类的对象所替代,而不会影响程序的正确性。换句话说,子类应该能够完全替换其父类,并且程序仍然能够正常工作。
实现该原则的关键在于确保子类在继承父类时,不会破坏父类中已经存在的行为和约束条件。也就是说,子类可以扩展父类的功能,但不能改变父类的行为。
遵循里氏代换原则可以提高代码的可扩展性和可维护性,减少代码的复杂性,并且使代码更容易测试和调试。同时,它也是实现开闭原则、单一职责原则和接口隔离原则等其他面向对象原则的基础。
有如下代码片断:
//类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。
try {
throw new ExampleB("b")
} catch(ExampleA e){
System.out.println("ExampleA");
} catch(Exception e){
System.out.println("Exception");
}
Q:请问执行此段代码的输出是什么?
输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的
异常)
Q:List、Set、Map 是否继承自 Collection 接口?
List、Set 是,而Map 不是。Map 是键值对映射容器,与 List 和 Set 有明显的区别,而 Set 存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。
Q:阐述 ArrayList、Vector、LinkedList 的存储性能和特性。
ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉
及数组元素移动等内存操作,所以索引数据快而插入数据慢,
Vector 中的方法由于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上较ArrayList 差,因此已经是 Java 中的遗留容器。
LinkedList 使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector 属于遗留容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用。
但是由于 ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。 如下所示:
下面是一个使用Collections.synchronizedList方法将List转换为线程安全的示例代码:
copy codeimport java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
List<String> synchronizedList = Collections.synchronizedList(list);
// 在多线程环境下遍历synchronizedList
Runnable task = () -> {
synchronized(synchronizedList) {
for (String item : synchronizedList) {
System.out.println(item);
}
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
下面是一个使用Collections.synchronizedList方法将LinkedList转换为线程安全的示例代码:
copy codeimport java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class SynchronizedLinkedListExample {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("apple");
linkedList.add("banana");
linkedList.add("orange");
List<String> synchronizedList = Collections.synchronizedList(linkedList);
// 在多线程环境下遍历synchronizedList
Runnable task = () -> {
synchronized(synchronizedList) {
for (String item : synchronizedList) {
System.out.println(item);
}
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
上面的代码分别创建了一个普通的ArrayList和LinkedList,并向其中添加了三个元素。然后,使用Collections.synchronizedList方法将该List/LinkedList转换为线程安全的List/LinkedList。最后,创建两个线程来同时遍历该List/LinkedList,并且在遍历时使用synchronized关键字来保证线程安全。
需要注意的是,虽然使用了synchronizedList方法可以将List转换为线程安全的List,但是在对该List进行遍历、添加、删除等操作时,仍然需要使用synchronized关键字来保证线程安全。
Q:Collection 和 Collections 的区别?
- Collection 是一个接口,它是 Set、List 等容器的父接口;
- Collections 是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等
Q:List、Map、Set 三个接口存取元素时,各有什么特点?
List 以特定索引来存取元素,可以有重复元素。
Set 不能存放重复元素(用对象的equals()方法来区分元素是否重复)。
Map 保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。
Set 和 Map 容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为 O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。
Q:TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?
- TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。
- TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。Collections 工具类的 sort 方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
- Collections 工具类中的 sort(),不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对匿名内部类回调模式的应用(Java 中对函数式编程的支持)
例子 1:
import java.util.Set;
import java.util.TreeSet;
class Test01 {
public static void main(String[] args) {
Set<Student> set = new TreeSet<>(); // Java 7 的钻石语法 (构造器后面的尖括号中不需要写类型)
set.add(new Student("Hao LUO", 33));
set.add(new Student("XJ WANG", 32));
set.add(new Student("Bruce LEE", 60));
set.add(new Student("Bob YANG", 22));
for (Student stu : set) {
System.out.println(stu);
}
// 输出结果:
// Student [name=Bob YANG, age=22]
// Student [name=XJ WANG, age=32]
// Student [name=Hao LUO, age=33]
// Student [name=Bruce LEE, age=60]
}
}
public class Student implements Comparable<Student> {
private String name; // 姓名
private int age; // 年龄
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Student o) {
return this.age - o.age; // 比较年龄(年龄的升序)
}
}
例子 2:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Test02 {
public static void main(String[] args) {
List<Student> list = new ArrayList<>(); // Java 7 的钻石语法 (构造器后面的尖括号中不需要写类型)
list.add(new Student("Hao LUO", 33));
list.add(new Student("XJ WANG", 32));
list.add(new Student("Bruce LEE", 60));
list.add(new Student("Bob YANG", 22));
// 通过 sort 方法的第二个参数传入一个 Comparator 接口对象
// 相当于是传入一个比较对象大小的算法到 sort 方法中
// 由于 Java 中没有函数指针、仿函数、委托这样的概念
// 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName()); // 比较学生姓名
}
});
for (Student stu : list) {
System.out.println(stu);
}
// 输出结果:
// Student [name=Bob YANG, age=22]
// Student [name=Bruce LEE, age=60]第 267 页 共 485 页
// Student [name=Hao LUO, age=33]
// Student [name=XJ WANG, age=32]
}
}
public class Student {
private String name; // 姓名
private int age; // 年龄
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取学生姓名
*/
public String getName() {
return name;
}
/**
* 获取学生年龄
*/
public int getAge() {
return age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
Q:Thread 类的 sleep()方法和对象的 wait()方法都可以让线程暂停执行,它们有什么区别?
sleep()方法 ( 休眠 ) 是线程类 ( Thread) 的静态方法 , 调用此方法会让当前线程暂停执行指定的时间,将执行机会( CPU)让给其他线程,但是对象的锁依然保持 ,因此休眠时间结束后会自动恢复( 线程回到就绪状态 ,请参考第 66 题中的线程状态转换图)。
wait()是 Object 类的方法,调用对象的 wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池( wait pool),只有调用对象的 notify()方法( 或 notifyAll()方法 )时才能唤醒等待池中的线程进入等锁池( lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
Q:线程和进程
可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。
简单的说:
- 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;
- 线程是进程的一个实体 , 是 CPU 调度和分派的基本单位 , 是比进程更小的能独立运行的基本单位。
- 线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的 , 因为它可能占用了更多的 CPU 资源 。
- 当然 , 也不是线程越多 , 程序的性能就越好,因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js就采用了单线程异步 I/O 的工作模式。
Q:线程的 sleep()方法和 yield()方法有什么区别
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的
机会;
② 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
④ sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。
Q:当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?
不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入
A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁