diff --git a/README.md b/README.md index e75c768..65cb15f 100644 --- a/README.md +++ b/README.md @@ -1,6426 +1,951 @@ -关注公众号『**Java专栏**』,发送『面试』 获取该项目完整PDF - -![公众号:java专栏](qr_code.jpg) - ---- - - - -- [基础篇](#基础篇) - - [1、 Java语言有哪些特点](#1-java语言有哪些特点) - - [2、面向对象和面向过程的区别](#2面向对象和面向过程的区别) - - [3 、八种基本数据类型的大小,以及他们的封装类](#3-八种基本数据类型的大小以及他们的封装类) - - [4、标识符的命名规则。](#4标识符的命名规则) - - [5、instanceof 关键字的作用](#5instanceof-关键字的作用) - - [6、Java自动装箱与拆箱](#6java自动装箱与拆箱) - - [7、 重载和重写的区别](#7-重载和重写的区别) - - [8、 equals与==的区别](#8-equals与的区别) - - [9、 Hashcode的作用](#9-hashcode的作用) - - [10、String、String StringBuffer 和 StringBuilder 的区别是什么?](#10stringstring-stringbuffer-和-stringbuilder-的区别是什么) - - [11、ArrayList和linkedList的区别](#11arraylist和linkedlist的区别) - - [12、 HashMap和HashTable的区别](#12-hashmap和hashtable的区别) - - [13、 Collection包结构,与Collections的区别](#13-collection包结构与collections的区别) - - [14、 Java的四种引用,强弱软虚](#14-java的四种引用强弱软虚) - - [15、 泛型常用特点](#15-泛型常用特点) - - [16、Java创建对象有几种方式?](#16java创建对象有几种方式) - - [17、有没有可能两个不相等的对象有相同的hashcode](#17有没有可能两个不相等的对象有相同的hashcode) - - [18、深拷贝和浅拷贝的区别是什么?](#18深拷贝和浅拷贝的区别是什么) - - [19、final有哪些用法?](#19final有哪些用法) - - [20、static都有哪些用法?](#20static都有哪些用法) - - [21、3*0.1`==`0.3返回值是什么](#2130103返回值是什么) - - [22、a=a+b与a+=b有什么区别吗?](#22aab与ab有什么区别吗) - - [23、try catch finally,try里有return,finally还执行么?](#23try-catch-finallytry里有returnfinally还执行么) - - [24、 Excption与Error包结构](#24-excption与error包结构) - - [25、OOM你遇到过哪些情况,SOF你遇到过哪些情况](#25oom你遇到过哪些情况sof你遇到过哪些情况) - - [26、 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#26-简述线程程序进程的基本概念以及他们之间关系是什么) - - [27、线程有哪些基本状态?](#27线程有哪些基本状态) - - [28、Java 序列化中如果有些字段不想进行序列化,怎么办?](#28java-序列化中如果有些字段不想进行序列化怎么办) - - [29、Java 中 IO 流](#29java-中-io-流) - - [30、 Java IO与 NIO的区别](#30-java-io与-nio的区别) - - [31、java反射的作用于原理](#31java反射的作用于原理) - - [32、说说List,Set,Map三者的区别?](#32说说listsetmap三者的区别) -- [JVM篇](#jvm篇) - - [1、知识点汇总](#1知识点汇总) - - [2、知识点详解:](#2知识点详解) - - [3、类加载与卸载](#3类加载与卸载) - - [4、简述一下JVM的内存模型](#4简述一下jvm的内存模型) - - [线程私有区](#线程私有区) - - [线程共享区](#线程共享区) - - [5、堆和栈的区别](#5堆和栈的区别) - - [6、什么时候会触发FullGC](#6什么时候会触发fullgc) - - [7、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?](#7什么是java虚拟机为什么java被称作是平台无关的编程语言) - - [8、Java内存结构](#8java内存结构) - - [9、对象分配规则](#9对象分配规则) - - [10、描述一下JVM加载class文件的原理机制?](#10描述一下jvm加载class文件的原理机制) - - [11、Java对象创建过程](#11java对象创建过程) - - [12、类的生命周期](#12类的生命周期) - - [13、简述Java的对象结构](#13简述java的对象结构) - - [14、如何判断对象可以被回收?](#14如何判断对象可以被回收) - - [15、JVM的永久代中会发生垃圾回收么?](#15jvm的永久代中会发生垃圾回收么) - - [16、垃圾收集算法](#16垃圾收集算法) - - [17、调优命令有哪些?](#17调优命令有哪些) - - [18、调优工具](#18调优工具) - - [19、Minor GC与Full GC分别在什么时候发生?](#19minor-gc与full-gc分别在什么时候发生) - - [20、你知道哪些JVM性能调优](#20你知道哪些jvm性能调优) - - [21、JVM内存分哪几个区,每个区的作用是什么?](#21jvm内存分哪几个区每个区的作用是什么) - - [22、简述Java垃圾回收机制?](#22简述java垃圾回收机制) - - [23、什么是类加载器,类加载器有哪些?](#23什么是类加载器类加载器有哪些) - - [24、你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获?](#24你有没有遇到过outofmemory问题你是怎么来处理这个问题的处理-过程中有哪些收获) - - [25、JDK 1.8之后Perm Space有哪些变动? MetaSpace⼤⼩默认是⽆限的么? 还是你们会通过什么⽅式来指定⼤⼩?](#25jdk-18之后perm-space有哪些变动-metaspace⼤⼩默认是⽆限的么-还是你们会通过什么⽅式来指定⼤⼩) - - [26、跟JVM内存相关的几个核心参数图解](#26跟jvm内存相关的几个核心参数图解) - - [27、如何启动系统的时候设置JVM的启动参数](#27如何启动系统的时候设置jvm的启动参数) -- [多线程&并发篇](#多线程并发篇) - - [1、Java中实现多线程有几种方法](#1java中实现多线程有几种方法) - - [2、如何停止一个正在运行的线程](#2如何停止一个正在运行的线程) - - [3、notify()和notifyAll()有什么区别?](#3notify和notifyall有什么区别) - - [4、sleep()和wait() 有什么区别?](#4sleep和wait-有什么区别) - - [5、volatile 是什么?可以保证有序性吗?](#5volatile-是什么可以保证有序性吗) - - [6、Thread 类中的start() 和 run() 方法有什么区别?](#6thread-类中的start-和-run-方法有什么区别) - - [7、为什么wait, notify 和 notifyAll这些方法不在thread类里面?](#7为什么wait-notify-和-notifyall这些方法不在thread类里面) - - [8、为什么wait和notify方法要在同步块中调用?](#8为什么wait和notify方法要在同步块中调用) - - [9、Java中interrupted 和 isInterruptedd方法的区别?](#9java中interrupted-和-isinterruptedd方法的区别) - - [10、Java中synchronized 和 ReentrantLock 有什么不同?](#10java中synchronized-和-reentrantlock-有什么不同) - - [11、有三个线程T1,T2,T3,如何保证顺序执行?](#11有三个线程t1t2t3如何保证顺序执行) - - [**12、SynchronizedMap和ConcurrentHashMap有什么区别?**](#12synchronizedmap和concurrenthashmap有什么区别) - - [13、什么是线程安全](#13什么是线程安全) - - [14、Thread类中的yield方法有什么作用?](#14thread类中的yield方法有什么作用) - - [15、Java线程池中submit() 和 execute()方法有什么区别?](#15java线程池中submit-和-execute方法有什么区别) - - [16、说一说自己对于 synchronized 关键字的了解](#16说一说自己对于-synchronized-关键字的了解) - - [17、说说自己是怎么使用 synchronized 关键字,在项目中用到了吗synchronized关键字最主要的三种使用方式:](#17说说自己是怎么使用-synchronized-关键字在项目中用到了吗synchronized关键字最主要的三种使用方式) - - [18、什么是线程安全?Vector是一个线程安全类吗?](#18什么是线程安全vector是一个线程安全类吗) - - [19、 **volatile关键字的作用?**](#19-volatile关键字的作用) - - [20、常用的线程池有哪些?](#20常用的线程池有哪些) - - [21、简述一下你对线程池的理解](#21简述一下你对线程池的理解) - - [22、Java程序是如何执行的](#22java程序是如何执行的) - - [23、说一说自己对于 synchronized 关键字的了解](#23说一说自己对于-synchronized-关键字的了解) - - [24、说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#24说说自己是怎么使用-synchronized-关键字在项目中用到了吗) - - [25、 讲一下 synchronized 关键字的底层原理](#25-讲一下-synchronized-关键字的底层原理) - - [26、 为什么要用线程池?](#26-为什么要用线程池) - - [27、 实现Runnable接口和Callable接口的区别](#27-实现runnable接口和callable接口的区别) - - [28、 执行execute()方法和submit()方法的区别是什么呢?](#28-执行execute方法和submit方法的区别是什么呢) - - [29、 如何创建线程池](#29-如何创建线程池) -- [Spring篇](#spring篇) - - [1、 Spring的IOC和AOP机制?](#1-spring的ioc和aop机制) - - [2、 Spring中Autowired和Resource关键字的区别?](#2-spring中autowired和resource关键字的区别) - - [3、依赖注入的方式有几种,各是什么?](#3依赖注入的方式有几种各是什么) - - [4、讲一下什么是Spring](#4讲一下什么是spring) - - [5、Spring MVC流程](#5spring-mvc流程) - - [6、SpringMVC怎么样设定重定向和转发的?](#6springmvc怎么样设定重定向和转发的) - - [7、 **SpringMVC常用的注解有哪些?**](#7--springmvc常用的注解有哪些) - - [8、 **Spring的AOP理解:**](#8-spring的aop理解) - - [9、Spring的IOC理解](#9spring的ioc理解) - - [10、解释一下spring bean的生命周期](#10解释一下spring-bean的生命周期) - - [11、 **解释Spring支持的几种bean的作用域。**](#11-解释spring支持的几种bean的作用域) - - [12、 **Spring基于xml注入bean的几种方式:**](#12-spring基于xml注入bean的几种方式) - - [13、Spring框架中都用到了哪些设计模式?](#13spring框架中都用到了哪些设计模式) -- [MyBatis篇](#mybatis篇) - - [1、什么是MyBatis](#1什么是mybatis) - - [2、MyBatis的优点和缺点](#2mybatis的优点和缺点) - - [3、#{}和${}的区别是什么?](#3和的区别是什么) - - [4、当实体类中的属性名和表中的字段名不一样 ,怎么办 ?](#4当实体类中的属性名和表中的字段名不一样-怎么办-) - - [5、Mybatis是如何进行分页的?分页插件的原理是什么?](#5mybatis是如何进行分页的分页插件的原理是什么) - - [6、Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?](#6mybatis是如何将sql执行结果封装为目标对象并返回的都有哪些映射形式) - - [7、 如何执行批量插入?](#7-如何执行批量插入) - - [8、Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?](#8xml映射文件中除了常见的selectinsertupdaedelete标签之外还有哪些标签) - - [9、MyBatis实现一对一有几种方式?具体怎么操作的?](#9mybatis实现一对一有几种方式具体怎么操作的) - - [10、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?](#10mybatis是否支持延迟加载如果支持它的实现原理是什么) - - [11、Mybatis的一级、二级缓存:](#11mybatis的一级二级缓存) -- [SpringBoot篇](#springboot篇) - - [1、什么是SpringBoot?为什么要用SpringBoot](#1什么是springboot为什么要用springboot) - - [2、Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?](#2spring-boot-的核心注解是哪个它主要由哪几个注解组成的) - - [3、运行Spring Boot有哪几种方式?](#3运行spring-boot有哪几种方式) - - [4、如何理解 Spring Boot 中的 Starters?](#4如何理解-spring-boot-中的-starters) - - [5、 如何在Spring Boot启动的时候运行一些特定的代码?](#5-如何在spring-boot启动的时候运行一些特定的代码) - - [6、 **Spring Boot 需要独立的容器运行吗?**](#6-spring-boot-需要独立的容器运行吗) - - [7、 **Spring Boot中的监视器是什么?**](#7-spring-boot中的监视器是什么) - - [8、 **如何使用Spring Boot实现异常处理?**](#8-如何使用spring-boot实现异常处理) - - [9、 **你如何理解 Spring Boot 中的 Starters?**](#9-你如何理解-spring-boot-中的-starters) - - [10、 **springboot常用的starter有哪些**](#10-springboot常用的starter有哪些) - - [11、 **SpringBoot 实现热部署有哪几种方式?**](#11-springboot-实现热部署有哪几种方式) - - [12、 **如何理解 Spring Boot 配置加载顺序?**](#12-如何理解-spring-boot-配置加载顺序) - - [13、 **Spring Boot 的核心配置文件有哪几个?它们的区别是什么?**](#13-spring-boot-的核心配置文件有哪几个它们的区别是什么) - - [14、如何集成 Spring Boot 和 ActiveMQ?](#14如何集成-spring-boot-和-activemq) - - [15、如何重新加载Spring Boot上的更改,而无需重新启动服务器?](#15如何重新加载spring-boot上的更改而无需重新启动服务器) - - [16、 Spring Boot、Spring MVC 和 Spring 有什么区别?](#16-spring-bootspring-mvc-和-spring-有什么区别) - - [17、 **能否举一个例子来解释更多 Staters 的内容?**](#17-能否举一个例子来解释更多-staters-的内容) - - [18、 **Spring Boot 还提供了其它的哪些 Starter Project Options?**](#18-spring-boot-还提供了其它的哪些-starter-project-options) -- [MySQL篇](#mysql篇) - - [1、数据库的三范式是什么](#1数据库的三范式是什么) - - [2、数据库引擎有哪些](#2数据库引擎有哪些) - - [3、InnoDB与MyISAM的区别](#3innodb与myisam的区别) - - [4、数据库的事务](#4数据库的事务) - - [5、索引问题](#5索引问题) - - [6、SQL优化](#6sql优化) - - [7、简单说一说drop、delete与truncate的区别](#7简单说一说dropdelete与truncate的区别) - - [8、什么是视图](#8什么是视图) - - [9、 什么是内联接、左外联接、右外联接?](#9-什么是内联接左外联接右外联接) - - [10、并发事务带来哪些问题?](#10并发事务带来哪些问题) - - [11、事务隔离级别有哪些?MySQL的默认隔离级别是?](#11事务隔离级别有哪些mysql的默认隔离级别是) - - [12、大表如何优化?](#12大表如何优化) - - [1. 限定数据的范围](#1-限定数据的范围) - - [2. 读/写分离](#2-读写分离) - - [3. 垂直分区](#3-垂直分区) - - [4. 水平分区](#4-水平分区) - - [13、分库分表之后,id 主键如何处理?](#13分库分表之后id-主键如何处理) - - [14、mysql有关权限的表都有哪几个](#14mysql有关权限的表都有哪几个) - - [15、mysql有哪些数据类型](#15mysql有哪些数据类型) - - [16、创建索引的三种方式,删除索引](#16创建索引的三种方式删除索引) -- [Redis篇](#redis篇) - - [1、Redis持久化机制](#1redis持久化机制) - - [2、缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题](#2缓存雪崩缓存穿透缓存预热缓存更新缓存降级等问题) - - [3、热点数据和冷数据是什么](#3热点数据和冷数据是什么) - - [4、Memcache与Redis的区别都有哪些?](#4memcache与redis的区别都有哪些) - - [5、单线程的redis为什么这么快](#5单线程的redis为什么这么快) - - [6、redis的数据类型,以及每种数据类型的使用场景](#6redis的数据类型以及每种数据类型的使用场景) - - [7、redis的过期策略以及内存淘汰机制](#7redis的过期策略以及内存淘汰机制) - - [8、Redis 为什么是单线程的](#8redis-为什么是单线程的) - - [9、Redis 常见性能问题和解决方案?](#9redis-常见性能问题和解决方案) - - [10、为什么Redis的操作是原子性的,怎么保证原子性的?](#10为什么redis的操作是原子性的怎么保证原子性的) - - [11、Redis事务](#11redis事务) -- [SpringCloud篇](#springcloud篇) - - [1、什么是SpringCloud](#1什么是springcloud) - - [2、什么是微服务](#2什么是微服务) - - [3、SpringCloud有什么优势](#3springcloud有什么优势) - - [4、 **什么是服务熔断?什么是服务降级?**](#4-什么是服务熔断什么是服务降级) - - [5、 **Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?**](#5-eureka和zookeeper都可以提供服务注册与发现的功能请说说两个的区别) - - [6、SpringBoot和SpringCloud的区别?](#6springboot和springcloud的区别) - - [7、负载平衡的意义什么?](#7负载平衡的意义什么) - - [8、什么是Hystrix?它如何实现容错?](#8什么是hystrix它如何实现容错) - - [9、什么是Hystrix断路器?我们需要它吗?](#9什么是hystrix断路器我们需要它吗) - - [10、说说 RPC 的实现原理](#10说说-rpc-的实现原理) -- [Nginx篇](#nginx篇) - - [1、简述一下什么是Nginx,它有什么优势和功能?](#1简述一下什么是nginx它有什么优势和功能) - - [2、Nginx是如何处理一个HTTP请求的呢?](#2nginx是如何处理一个http请求的呢) - - [3、列举一些Nginx的特性](#3列举一些nginx的特性) - - [4、请列举Nginx和Apache 之间的不同点](#4请列举nginx和apache-之间的不同点) - - [5、在Nginx中,如何使用未定义的服务器名称来阻止处理请求?](#5在nginx中如何使用未定义的服务器名称来阻止处理请求) - - [6、请解释Nginx服务器上的Master和Worker进程分别是什么?](#6请解释nginx服务器上的master和worker进程分别是什么) - - [7、请解释代理中的正向代理和反向代理](#7请解释代理中的正向代理和反向代理) - - [8、解释Nginx用途](#8解释nginx用途) -- [zookeeper篇](#zookeeper篇) - - [1\. ZooKeeper 是什么?](#1\-zookeeper-是什么) - - [2\. ZooKeeper 提供了什么?](#2\-zookeeper-提供了什么) - - [3.Zookeeper 文件系统](#3zookeeper-文件系统) - - [4\. ZAB 协议?](#4\-zab-协议) - - [5\. 四种类型的数据节点 Znode](#5\-四种类型的数据节点-znode) - - [6\. Zookeeper Watcher 机制 -- 数据变更通知](#6\-zookeeper-watcher-机制----数据变更通知) - - [7\. 客户端注册 Watcher 实现](#7\-客户端注册-watcher-实现) - - [8\. 服务端处理 Watcher 实现](#8\-服务端处理-watcher-实现) - - [9\. 客户端回调 Watcher](#9\-客户端回调-watcher) - - [10\. ACL 权限控制机制](#10\-acl-权限控制机制) - - [11\. Chroot 特性](#11\-chroot-特性) - - [12\. 会话管理](#12\-会话管理) - - [13\. 服务器角色](#13\-服务器角色) - - [14\. Zookeeper 下 Server 工作状态](#14\-zookeeper-下-server-工作状态) - - [15\. 数据同步](#15\-数据同步) - - [16\. zookeeper 是如何保证事务的顺序一致性的?](#16\-zookeeper-是如何保证事务的顺序一致性的) - - [17\. 分布式集群中为什么会有 Master?](#17\-分布式集群中为什么会有-master) - - [18\. zk 节点宕机如何处理?](#18\-zk-节点宕机如何处理) - - [19\. zookeeper 负载均衡和 nginx 负载均衡区别](#19\-zookeeper-负载均衡和-nginx-负载均衡区别) - - [20\. Zookeeper 有哪几种几种部署模式?](#20\-zookeeper-有哪几种几种部署模式) - - [21\. 集群最少要几台机器,集群规则是怎样的?](#21\-集群最少要几台机器集群规则是怎样的) - - [22\. 集群支持动态添加机器吗?](#22\-集群支持动态添加机器吗) - - [23\. Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的?](#23\-zookeeper-对节点的-watch-监听通知是永久的吗为什么不是永久的) - - [24\. Zookeeper 的 java 客户端都有哪些?](#24\-zookeeper-的-java-客户端都有哪些) - - [25\. chubby 是什么,和 zookeeper 比你怎么看?](#25\-chubby-是什么和-zookeeper-比你怎么看) - - [26\. 说几个 zookeeper 常用的命令。](#26\-说几个-zookeeper-常用的命令) - - [27\. ZAB 和 Paxos 算法的联系与区别?](#27\-zab-和-paxos-算法的联系与区别) - - [28\. Zookeeper 的典型应用场景](#28\-zookeeper-的典型应用场景) -- [kafka篇](#kafka篇) - - [1、如何获取 topic 主题的列表](#1如何获取-topic-主题的列表) - - [2、生产者和消费者的命令行是什么?](#2生产者和消费者的命令行是什么) - - [3、consumer 是推还是拉?](#3consumer-是推还是拉) - - [4、讲讲 kafka 维护消费状态跟踪的方法](#4讲讲-kafka-维护消费状态跟踪的方法) - - [5、讲一下主从同步](#5讲一下主从同步) - - [6、为什么需要消息系统,mysql 不能满足需求吗?](#6为什么需要消息系统mysql-不能满足需求吗) - - [7、Zookeeper 对于 Kafka 的作用是什么?](#7zookeeper-对于-kafka-的作用是什么) - - [9、Kafka 判断一个节点是否还活着有那两个条件?](#9kafka-判断一个节点是否还活着有那两个条件) - - [10、Kafka 与传统 MQ 消息系统之间有三个关键区别](#10kafka-与传统-mq-消息系统之间有三个关键区别) - - [11、讲一讲 kafka 的 ack 的三种机制](#11讲一讲-kafka-的-ack-的三种机制) - - [12、消费者如何不自动提交偏移量,由应用提交?](#12消费者如何不自动提交偏移量由应用提交) - - [13、消费者故障,出现活锁问题如何解决?](#13消费者故障出现活锁问题如何解决) - - [14、如何控制消费的位置](#14如何控制消费的位置) - - [15、kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费?](#15kafka-分布式不是单机的情况下如何保证消息的顺序消费) - - [16、kafka 的高可用机制是什么?](#16kafka-的高可用机制是什么) - - [17、kafka 如何减少数据丢失](#17kafka-如何减少数据丢失) - - [18、kafka 如何不消费重复数据?比如扣款,我们不能重复的扣。](#18kafka-如何不消费重复数据比如扣款我们不能重复的扣) -- [MQ篇](#mq篇) - - [1、为什么使用MQ](#1为什么使用mq) - - [2、MQ优缺点](#2mq优缺点) - - [3、Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别?](#3kafkaactivemqrabbitmqrocketmq-都有什么区别) - - [4、如何保证高可用的?](#4如何保证高可用的) - - [5、如何保证消息的可靠传输?如果消息丢了怎么办](#5如何保证消息的可靠传输如果消息丢了怎么办) - - [6、如何保证消息的顺序性](#6如何保证消息的顺序性) - - [7、 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?](#7-如何解决消息队列的延时以及过期失效问题消息队列满了以后该怎么处理有几百万消息持续积压几小时说说怎么解决) - - [8、设计MQ的思路](#8设计mq的思路) -- [Elasticsearch篇](#elasticsearch篇) - - [1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。](#1elasticsearch-了解多少说说你们公司-es-的集群架构索引数据大小分片有多少以及一些调优手段-) - - [1.1、设计阶段调优](#11设计阶段调优) - - [1.2、写入调优](#12写入调优) - - [1.3、查询调优](#13查询调优) - - [1.4、其他调优](#14其他调优) - - [2、elasticsearch 的倒排索引是什么](#2elasticsearch-的倒排索引是什么) - - [3、elasticsearch 索引数据多了怎么办,如何调优,部署](#3elasticsearch-索引数据多了怎么办如何调优部署) - - [3.1 动态索引层面](#31-动态索引层面) - - [3.2 存储层面](#32-存储层面) - - [3.3 部署层面](#33-部署层面) - - [4、elasticsearch 是如何实现 master 选举的](#4elasticsearch-是如何实现-master-选举的) - - [5、详细描述一下 Elasticsearch 索引文档的过程](#5详细描述一下-elasticsearch-索引文档的过程) - - [6、详细描述一下 Elasticsearch 搜索的过程?](#6详细描述一下-elasticsearch-搜索的过程) - - [7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法](#7elasticsearch-在部署时对-linux-的设置有哪些优化方法) - - [8、lucence 内部结构是什么?](#8lucence-内部结构是什么) - - [9、Elasticsearch 是如何实现 Master 选举的?](#9elasticsearch-是如何实现-master-选举的) - - [10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个](#10elasticsearch-中的节点比如共-20-个其中的-10-个) - - [11、客户端在和集群连接时,如何选择特定的节点执行请求的?](#11客户端在和集群连接时如何选择特定的节点执行请求的) - - [12、详细描述一下 Elasticsearch 索引文档的过程。](#12详细描述一下-elasticsearch-索引文档的过程) - - [13、详细描述一下 Elasticsearch 更新和删除文档的过程。](#13详细描述一下-elasticsearch-更新和删除文档的过程) - - [14、详细描述一下 Elasticsearch 搜索的过程。](#14详细描述一下-elasticsearch-搜索的过程) - - [15、在 Elasticsearch 中,是怎么根据一个词找到对应的倒排索引的?](#15在-elasticsearch-中是怎么根据一个词找到对应的倒排索引的) - - [16、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法?](#16elasticsearch-在部署时对-linux-的设置有哪些优化方法) - - [17、对于 GC 方面,在使用 Elasticsearch 时要注意什么?](#17对于-gc-方面在使用-elasticsearch-时要注意什么) - - [18、Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?](#18elasticsearch-对于大数据量上亿量级的聚合如何实现) - - [19、在并发情况下,Elasticsearch 如果保证读写一致?](#19在并发情况下elasticsearch-如果保证读写一致) - - [20、如何监控 Elasticsearch 集群状态?](#20如何监控-elasticsearch-集群状态) - - [21、介绍下你们电商搜索的整体技术架构。](#21介绍下你们电商搜索的整体技术架构) - - [22、介绍一下你们的个性化搜索方案?](#22介绍一下你们的个性化搜索方案) - - [23、是否了解字典树?](#23是否了解字典树) - - [24、拼写纠错是如何实现的?](#24拼写纠错是如何实现的) -- [Linux篇](#linux篇) - - [1、绝对路径用什么符号表示?当前目录、上层目录用什么表示?主目录用什么表示? 切换目录用什么命令?](#1绝对路径用什么符号表示当前目录上层目录用什么表示主目录用什么表示-切换目录用什么命令) - - [2、怎么查看当前进程?怎么执行退出?怎么查看当前路径?](#2怎么查看当前进程怎么执行退出怎么查看当前路径) - - [3、怎么清屏?怎么退出当前命令?怎么执行睡眠?怎么查看当前用户 id?查看指定帮助用什么命令?](#3怎么清屏怎么退出当前命令怎么执行睡眠怎么查看当前用户-id查看指定帮助用什么命令) - - [4、Ls 命令执行什么功能? 可以带哪些参数,有什么区别?](#4ls-命令执行什么功能-可以带哪些参数有什么区别) - - [5、建立软链接(快捷方式),以及硬链接的命令。](#5建立软链接快捷方式以及硬链接的命令) - - [6、目录创建用什么命令?创建文件用什么命令?复制文件用什么命令?](#6目录创建用什么命令创建文件用什么命令复制文件用什么命令) - - [7、查看文件内容有哪些命令可以使用?](#7查看文件内容有哪些命令可以使用) - - [8、随意写文件命令?怎么向屏幕输出带空格的字符串,比如”hello world”?](#8随意写文件命令怎么向屏幕输出带空格的字符串比如hello-world) - - [9、终端是哪个文件夹下的哪个文件?黑洞文件是哪个文件夹下的哪个命令?](#9终端是哪个文件夹下的哪个文件黑洞文件是哪个文件夹下的哪个命令) - - [10、移动文件用哪个命令?改名用哪个命令?](#10移动文件用哪个命令改名用哪个命令) - - [11、复制文件用哪个命令?如果需要连同文件夹一块复制呢?如果需要有提示功能呢?](#11复制文件用哪个命令如果需要连同文件夹一块复制呢如果需要有提示功能呢) - - [12、删除文件用哪个命令?如果需要连目录及目录下文件一块删除呢?删除空文件夹用什么命令?](#12删除文件用哪个命令如果需要连目录及目录下文件一块删除呢删除空文件夹用什么命令) - - [13、Linux 下命令有哪几种可使用的通配符?分别代表什么含义?](#13linux-下命令有哪几种可使用的通配符分别代表什么含义) - - [14、用什么命令对一个文件的内容进行统计?(行号、单词数、字节数)](#14用什么命令对一个文件的内容进行统计行号单词数字节数) - - [15、Grep 命令有什么用? 如何忽略大小写? 如何查找不含该串的行?](#15grep-命令有什么用-如何忽略大小写-如何查找不含该串的行) - - [16、Linux 中进程有哪几种状态?在 ps 显示出来的信息中,分别用什么符号表示的?](#16linux-中进程有哪几种状态在-ps-显示出来的信息中分别用什么符号表示的) - - [17、怎么使一个命令在后台运行?](#17怎么使一个命令在后台运行) - - [18、利用 ps 怎么显示所有的进程? 怎么利用 ps 查看指定进程的信息?](#18利用-ps-怎么显示所有的进程-怎么利用-ps-查看指定进程的信息) - - [19、哪个命令专门用来查看后台任务?](#19哪个命令专门用来查看后台任务) - - [20、把后台任务调到前台执行使用什么命令?把停下的后台任务在后台执行起来用什么命令?](#20把后台任务调到前台执行使用什么命令把停下的后台任务在后台执行起来用什么命令) - - [21、终止进程用什么命令? 带什么参数?](#21终止进程用什么命令-带什么参数) - - [22、怎么查看系统支持的所有信号?](#22怎么查看系统支持的所有信号) - - [23、搜索文件用什么命令? 格式是怎么样的?](#23搜索文件用什么命令-格式是怎么样的) - - [24、查看当前谁在使用该主机用什么命令? 查找自己所在的终端信息用什么命令?](#24查看当前谁在使用该主机用什么命令-查找自己所在的终端信息用什么命令) - - [25、使用什么命令查看用过的命令列表?](#25使用什么命令查看用过的命令列表) - - [26、使用什么命令查看磁盘使用空间? 空闲空间呢?](#26使用什么命令查看磁盘使用空间-空闲空间呢) - - [27、使用什么命令查看网络是否连通?](#27使用什么命令查看网络是否连通) - - [28、使用什么命令查看 ip 地址及接口信息?](#28使用什么命令查看-ip-地址及接口信息) - - [29、查看各类环境变量用什么命令?](#29查看各类环境变量用什么命令) - - [30、通过什么命令指定命令提示符?](#30通过什么命令指定命令提示符) - - [31、查找命令的可执行文件是去哪查找的? 怎么对其进行设置及添加?](#31查找命令的可执行文件是去哪查找的-怎么对其进行设置及添加) - - [32、通过什么命令查找执行命令?](#32通过什么命令查找执行命令) - - [33、怎么对命令进行取别名?](#33怎么对命令进行取别名) - - [34、du 和 df 的定义,以及区别?](#34du-和-df-的定义以及区别) - - [35、awk 详解。](#35awk-详解) - - [36、当你需要给命令绑定一个宏或者按键的时候,应该怎么做呢?](#36当你需要给命令绑定一个宏或者按键的时候应该怎么做呢) - - [37、如果一个 linux 新手想要知道当前系统支持的所有命令的列表,他需要怎么做?](#37如果一个-linux-新手想要知道当前系统支持的所有命令的列表他需要怎么做) - - [38、如果你的助手想要打印出当前的目录栈,你会建议他怎么做?](#38如果你的助手想要打印出当前的目录栈你会建议他怎么做) - - [39、你的系统目前有许多正在运行的任务,在不重启机器的条件下,有什么方法可以把所有正在运行的进程移除呢?](#39你的系统目前有许多正在运行的任务在不重启机器的条件下有什么方法可以把所有正在运行的进程移除呢) - - [40、bash shell 中的 hash 命令有什么作用?](#40bash-shell-中的-hash-命令有什么作用) - - [41、哪一个 bash 内置命令能够进行数学运算。](#41哪一个-bash-内置命令能够进行数学运算) - - [42、怎样一页一页地查看一个大文件的内容呢?](#42怎样一页一页地查看一个大文件的内容呢) - - [43、数据字典属于哪一个用户的?](#43数据字典属于哪一个用户的) - - [44、怎样查看一个 linux 命令的概要与用法?假设你在/bin 目录中偶然看到一个你从没见过的的命令,怎样才能知道它的作用和用法呢?](#44怎样查看一个-linux-命令的概要与用法假设你在bin-目录中偶然看到一个你从没见过的的命令怎样才能知道它的作用和用法呢) - - [45、使用哪一个命令可以查看自己文件系统的磁盘空间配额呢?](#45使用哪一个命令可以查看自己文件系统的磁盘空间配额呢) - - [46、列举几个常用的Linux命令](#46列举几个常用的linux命令) - - [47、你平时是怎么查看日志的?](#47你平时是怎么查看日志的) -- [数据结构与算法篇](#数据结构与算法篇) - - [1、常用的数据结构](#1常用的数据结构) - - [1. 数组](#1-数组) - - [2. 栈](#2-栈) - - [3. 队列](#3-队列) - - [4. 链表](#4-链表) - - [5. 图](#5-图) - - [6. 树](#6-树) - - [7. 前缀树](#7-前缀树) - - [8. 哈希表](#8-哈希表) - - [2、 数据里有{1,2,3,4,5,6,7,8,9},请随机打乱顺序,生成一个新的数组(请以代码实现)](#2-数据里有123456789请随机打乱顺序生成一个新的数组请以代码实现) - - [3、 写出代码判断一个整数是不是2的阶次方(请代码实现,谢绝调用API方法)](#3-写出代码判断一个整数是不是2的阶次方请代码实现谢绝调用api方法) - - [4、 假设今日是2015年3月1日,星期日,请算出13个月零6天后是星期几,距离现在多少天(请用代码实现,谢绝调用API方法)](#4-假设今日是2015年3月1日星期日请算出13个月零6天后是星期几距离现在多少天请用代码实现谢绝调用api方法) - - [5、 有两个篮子,分别为A 和 B,篮子A里装有鸡蛋,篮子B里装有苹果,请用面向对象的思想实现两个篮子里的物品交换(请用代码实现)](#5-有两个篮子分别为a-和-b篮子a里装有鸡蛋篮子b里装有苹果请用面向对象的思想实现两个篮子里的物品交换请用代码实现) - - [6、更多算法练习](#6更多算法练习) -- [简历篇](#简历篇) - - [为什么说简历很重要?](#为什么说简历很重要) - - [先从面试来说](#先从面试来说) - - [再从面试说起](#再从面试说起) - - [必知必会的几点](#必知必会的几点) - - [必须了解的两大法则](#必须了解的两大法则) - - [项目经历怎么写](#项目经历怎么写) - - [专业技能怎么写](#专业技能怎么写) - - [排版注意事项](#排版注意事项) - - [其他一些小tips](#其他一些小tips) - - - -## 基础篇 - - - -### 1、 Java语言有哪些特点 - -1、简单易学、有丰富的类库 - -2、面向对象(Java最重要的特性,让程序耦合度更低,内聚性更高) - -3、与平台无关性(JVM是Java跨平台使用的根本) - -4、可靠安全 - -5、支持多线程 - +# 整理不易,您的Fork、Star 就是我的动力 +> 题库非常全面包含了 Java基础、容器、多线程、JVM、并发编程、MySQL、Redis、MongoDB、Spring、SpringBoot、Mybatis、SpringCloud、Dubbo、Zookeeper、Kafka、Nginx、MQ、网络、数据结构与算法、Linux等等 -### 2、面向对象和面向过程的区别 +👉 **Java面试题完整版附答案,高清PDF下载:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** -**面向过程**:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发 +👉 **Java面试题完整版附答案,高清PDF下载:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** -**面向对象**:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有**封装、继承、多态**的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。 +👉 **Java面试题完整版附答案,高清PDF下载:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** +# 面试题索引 -### 3 、八种基本数据类型的大小,以及他们的封装类 +| | | | | | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| [Java基础](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/Java%E5%9F%BA%E7%A1%80.md) | [Java集合&容器](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/Java%E9%9B%86%E5%90%88&%E5%AE%B9%E5%99%A8.md) | [JVM](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/JVM.md) | [Java并发编程](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B.md) | [字符串&&集合](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/%E5%AD%97%E7%AC%A6%E4%B8%B2&%E9%9B%86%E5%90%88.md) | +| [网络协议](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE.md) | [数据结构与算法](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95.md) | [MySQL](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/database/MySQL.md) | [Redis](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/database/Redis.md) | [MongoDB](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/database/MongoDB.md) | +| [1000行SQL命令](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/database/%E4%B8%80%E5%8D%83%E8%A1%8CMySQL%E5%91%BD%E4%BB%A4.md) | [52条SQL语句性能优化策略](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/database/52%E6%9D%A1SQL%E8%AF%AD%E5%8F%A5%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5.md) | [Linux常用命令](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/linux/Linux%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4.md) | [Linux面试题](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/linux/Linux%E9%9D%A2%E8%AF%95%E9%A2%98%20.md) | [Dubbo](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/middleware/Dubbo.md) | +| [ElasticSearch](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/middleware/ElasticSearch.md) | [Kafka](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/middleware/Kafka.md) | [Nginx](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/middleware/Nginx.md) | [MQ](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/middleware/RabbitMQ.md) | [Zookeeper](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/middleware/zookeeper.md) | +| [Spring](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/%E6%A1%86%E6%9E%B6/Spring.md) | [SpringBoot](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/%E6%A1%86%E6%9E%B6/SpringBoot.md) | [SpringCloud](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/%E6%A1%86%E6%9E%B6/SpringCloud.md) | [Spring常用注解](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/%E6%A1%86%E6%9E%B6/SpringBoot%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3.md) | [MyBatis](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/%E6%A1%86%E6%9E%B6/MyBatis.md) | +| [Git常用命令](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/other/Git%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4.md) | | | []() | []() | -| 基本类型 | 大小(字节) | 默认值 | 封装类 | -| -------- | ------------ | ------------ | --------- | -| byte | 1 | (byte)0 | Byte | -| short | 2 | (short)0 | Short | -| int | 4 | 0 | Integer | -| long | 8 | 0L | Long | -| float | 4 | 0.0f | Float | -| double | 8 | 0.0d | Double | -| boolean | - | false | Boolean | -| char | 2 | \u0000(null) | Character | +# Java基础概念 +### 1. Java的特点有哪些 +Java 语言是一种分布式的面向对象语言,具有面向对象、平台无关性、简单性、解释执行、多线程、安全性等很多特点,下面针对这些特点进行逐一介绍。 +**1. 面向对象** -注: +Java 是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包等均有很好的支持。为了简单起见,Java 只支持类之间的单继承,但是可以使用接口来实现多继承。使用 Java 语言开发程序,需要采用面向对象的思想设计程序和编写代码。 -  1.int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。 +**2. 平台无关性** -  2.基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。 +平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。 -  虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。 +Java 语言使用 Java 虚拟机机制屏蔽了具体平台的相关信息,使得 Java 语言编译的程序只需生成虚拟机上的目标代码,就可以在多种平台上不加修改地运行。 +**3. 简单性** +Java 语言的语法与 C 语言和 C++ 语言很相近,使得很多程序员学起来很容易。对 Java 来说,它舍弃了很多 C++ 中难以理解的特性,如操作符的重载和多继承等,而且 Java 语言不使用指针,加入了垃圾回收机制,解决了程序员需要管理内存的问题,使编程变得更加简单。 -### 4、标识符的命名规则。 +**4. 解释执行** -**标识符的含义:** -是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。 +Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。在运行文件时,Java 的解释器对这些字节码进行解释执行,执行过程中需要加入的类在连接阶段被载入到运行环境中。 -**命名规则:(硬性要求)** -标识符可以包含英文字母,0-9的数字,$以及_ -标识符不能以数字开头 -标识符不是关键字 +**5. 多线程** -**命名规范:(非硬性要求)** -类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 -变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 -方法名规范:同变量名。 +Java 语言是多线程的,这也是 Java 语言的一大特性,它必须由 Thread 类和它的子类来创建。Java 支持多个线程同时执行,并提供多线程之间的同步机制。任何一个线程都有自己的 run() 方法,要执行的方法就写在 run() 方法体内。 +**6. 分布式** +Java 语言支持 Internet 应用的开发,在 Java 的基本应用编程接口中就有一个网络应用编程接口,它提供了网络应用编程的类库,包括 URL、URLConnection、Socket 等。Java 的 RIM 机制也是开发分布式应用的重要手段。 -### 5、instanceof 关键字的作用 +**7. 健壮性** +Java 的强类型机制、异常处理、垃圾回收机制等都是 Java 健壮性的重要保证。对指针的丢弃是 Java 的一大进步。另外,Java 的异常机制也是健壮性的一大体现。 +**8. 高性能** - instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为: +Java 的高性能主要是相对其他高级脚本语言来说的,随着 JIT(Just in Time)的发展,Java 的运行速度也越来越高。 -```java -boolean result = obj instanceof Class -``` - -  其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。 - -  注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。 - -```java -int i = 0; -System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型 -System.out.println(i instanceof Object);//编译不通过 -``` - -```java -Integer integer = new Integer(1); -System.out.println(integer instanceof Integer);//true -``` - -```java -//false  ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。 -System.out.println(null instanceof Object); -``` - - - -### 6、Java自动装箱与拆箱 - -**装箱就是自动将基本数据类型转换为包装器类型(int-->Integer);调用方法:Integer的valueOf(int) 方法** - -**拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer的intValue方法 ** - - - -在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行: - -```java -Integer i = new Integer(10); -``` - -  而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了: - -```java -Integer i = 10; -``` - - - -***面试题1: 以下代码会输出什么?*** - -```java -public class Main { - public static void main(String[] args) { - - Integer i1 = 100; - Integer i2 = 100; - Integer i3 = 200; - Integer i4 = 200; - - System.out.println(i1==i2); - System.out.println(i3==i4); - } -} -``` - -运行结果: - -``` -true -false -``` - - - -为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现: - -```java -public static Integer valueOf(int i) { - if(i >= -128 && i <= IntegerCache.high) - return IntegerCache.cache[i + 128]; - else - return new Integer(i); - } -``` - - - - 其中IntegerCache类的实现为: - -```java -private static class IntegerCache { - static final int high; - static final Integer cache[]; - - static { - final int low = -128; - - // high value may be configured by property - int h = 127; - if (integerCacheHighPropValue != null) { - // Use Long.decode here to avoid invoking methods that - // require Integer's autoboxing cache to be initialized - int i = Long.decode(integerCacheHighPropValue).intValue(); - i = Math.max(i, 127); - // Maximum array size is Integer.MAX_VALUE - h = Math.min(i, Integer.MAX_VALUE - -low); - } - high = h; - - cache = new Integer[(high - low) + 1]; - int j = low; - for(int k = 0; k < cache.length; k++) - cache[k] = new Integer(j++); - } - - private IntegerCache() {} - } -``` - - - -从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。 - -上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。 - - - -***面试题2:以下代码输出什么?*** - -```java -public class Main { - public static void main(String[] args) { - - Double i1 = 100.0; - Double i2 = 100.0; - Double i3 = 200.0; - Double i4 = 200.0; - - System.out.println(i1==i2); - System.out.println(i3==i4); - } -} -``` - -运行结果: - -``` -false -false -``` - -原因: 在某个范围内的整型数值的个数是有限的,而浮点数却不是。 - - - -### 7、 重载和重写的区别 - -**重写(Override)** - -从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。 - -```java -public class Father { - - public static void main(String[] args) { - // TODO Auto-generated method stub - Son s = new Son(); - s.sayHello(); - } - - public void sayHello() { - System.out.println("Hello"); - } -} - -class Son extends Father{ - - @Override - public void sayHello() { - // TODO Auto-generated method stub - System.out.println("hello by "); - } - -} - -``` - -**重写 总结:** -1.发生在父类与子类之间 -2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同 -3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private) -4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常 - - - -**重载(Overload)** - -在一个类中,同名的方法如果有不同的参数列表(**参数类型不同、参数个数不同甚至是参数顺序不同**)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但**不能通过返回类型是否相同来判断重载**。 - -```java -public class Father { - - public static void main(String[] args) { - // TODO Auto-generated method stub - Father s = new Father(); - s.sayHello(); - s.sayHello("wintershii"); - - } - - public void sayHello() { - System.out.println("Hello"); - } - - public void sayHello(String name) { - System.out.println("Hello" + " " + name); - } -} -``` - - **重载 总结:** -1.重载Overload是一个类中多态性的一种表现 -2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序) -3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准 - - - -### 8、 equals与==的区别 - - **== :** - -== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。 - -1、比较的是操作符两端的操作数是否是同一个对象。 -2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。 -3、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如: -int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。 - - - -**equals**: - -equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。 - - - -总结: - -所有比较是否相等时,都是用equals 并且在对常量相比较时,把常量写在前面,因为使用object的equals object可能为null 则空指针 - -在阿里的代码规范中只使用equals ,阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来排查老代码使用“==”,替换成equals - - - -### 9、 Hashcode的作用 - -java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就会比较满。 - -于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。 - - hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。 - - - -### 10、String、String StringBuffer 和 StringBuilder 的区别是什么? - -String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。 - -```java -private final char value[]; -``` - -每次+操作 : 隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法 拼接+后面的字符。 - - - -StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder抽象类中我们可以看到 - -```java -/** - * The value is used for character storage. - */ - char[] value; -``` - - 他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。 另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 - - - -### 11、ArrayList和linkedList的区别 - -**Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。** - -Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据, (因为删除数据以后, 需要把后面所有的数据前移) - -**缺点:** 数组初始化必须指定初始化的长度, 否则报错 - -例如: - -```java -int[] a = new int[4];//推介使用int[] 这种方式初始化 - -int c[] = {23,43,56,78};//长度:4,索引范围:[0,3] -``` - - - -**List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection。** - -**List有两个重要的实现类:ArrayList和LinkedList** - -**ArrayList: 可以看作是能够自动增长容量的数组** - -**ArrayList的toArray方法返回一个数组** - -**ArrayList的asList方法返回一个列表** - -ArrayList底层的实现是Array, 数组扩容实现 - - - -**LinkList是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。** - - - -### 12、 HashMap和HashTable的区别 - -**1、两者父类不同** - -HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。 - -**2、对外提供的接口不同** - -Hashtable比HashMap多提供了elments() 和contains() 两个方法。 - elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。 - -contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue() 就只是调用了一下contains() 方法。 - -**3、对null的支持不同** - -Hashtable:key和value都不能为null。 - -HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个key值对应的value为null。 - -**4、安全性不同** - -HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己处理多线程的安全问题。 - -Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。 - -虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。 - -ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。 - -**5、初始容量大小和每次扩充容量大小不同** - -**6、计算hash值的方法不同** - -### 13、 Collection包结构,与Collections的区别 - -Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set; - -Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的Collection框架。 - - - -### 14、 Java的四种引用,强弱软虚 - -- 强引用 - - 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式: - - ```java - String str = new String("str"); - ``` - - - -- 软引用 - - 软引用在程序内存不足时,会被回收,使用方式: - - ```java - // 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, - // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T - SoftReference wrf = new SoftReference(new String("str")); - ``` - - 可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。 - -- 弱引用 - - 弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式: - - ```java - WeakReference wrf = new WeakReference(str); - ``` - - **可用场景:** Java源码中的`java.util.WeakHashMap`中的`key`就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。 - -- 虚引用 - - 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入`ReferenceQueue`中。注意哦,其它引用是被JVM回收后才被传入`ReferenceQueue`中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有`ReferenceQueue`,使用例子: - - ```java - PhantomReference prf = new PhantomReference(new String("str"), new ReferenceQueue<>()); - ``` - - 可用场景: 对象销毁前的一些操作,比如说资源释放等。**`Object.finalize()`虽然也可以做这类动作,但是这个方式即不安全又低效 - - **上诉所说的几类引用,都是指对象本身的引用,而不是指`Reference`的四个子类的引用(`SoftReference`等)。** - - - -### 15、 泛型常用特点 - -泛型是Java SE 1.5之后的特性, 《Java 核心技术》中对泛型的定义是: - -> “泛型” 意味着编写的代码可以被不同类型的对象所重用。 - - “泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素,如 - -```java -List iniData = new ArrayList<>() -``` - -使用泛型的好处? - - 以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。 - - - -### 16、Java创建对象有几种方式? - -java中提供了以下四种创建对象的方式: - -- new创建新对象 -- 通过反射机制 -- 采用clone机制 -- 通过序列化机制 - - - -### 17、有没有可能两个不相等的对象有相同的hashcode - -有可能.在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般有以下几种方式来处理: - -- 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储. -- 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入 -- 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突. - - - -### 18、深拷贝和浅拷贝的区别是什么? - -- 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象. - -- 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向被复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的对象都复制了一遍. - - -### 19、final有哪些用法? - -final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了: - -- 被final修饰的类不可以被继承 -- 被final修饰的方法不可以被重写 -- 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变. -- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率 -- 被final修饰的常量,在编译阶段会存入常量池中. - -除此之外,编译器对final域要遵守的两个重排序规则更好: - -在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 -初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序. - -### 20、static都有哪些用法? - -所有的人都知道static关键字这两个基本的用法:静态变量和静态方法.也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享. - -除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作: - -```java -public calss PreCache{ - static{ - //执行相关操作 - } -} -``` - -此外static也多用于修饰内部类,此时称之为静态内部类. - -最后一种用法就是静态导包,即`import static`.import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如: - -```java -import static java.lang.Math.*; - -public class Test{ - - public static void main(String[] args){ - //System.out.println(Math.sin(20));传统做法 - System.out.println(sin(20)); - } -} -``` - - - -### 21、3*0.1`==`0.3返回值是什么 - - false,因为有些浮点数不能完全精确的表示出来. - - - -### 22、a=a+b与a+=b有什么区别吗? - - `+=`操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换.如: - -```java -byte a = 127; -byte b = 127; -b = a + b; // 报编译错误:cannot convert from int to byte -b += a; -``` - -以下代码是否有错,有的话怎么改? - -```java -short s1= 1; -s1 = s1 + 1; -``` - - 有错误.short类型在进行运算时会自动提升为int类型,也就是说`s1+1`的运算结果是int类型,而s1是short类型,此时编译器会报错. - -正确写法: - -```java -short s1= 1; -s1 += 1; -``` - - +=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错. - - - -### 23、try catch finally,try里有return,finally还执行么? - - 执行,并且finally的执行早于try里面的return - -结论: - -1、不管有木有出现异常,finally块中代码都会执行; - -2、当try和catch中有return时,finally仍然会执行; - -3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的; - -4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。 - - - -### 24、 Excption与Error包结构 - - Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常(RuntimeException),错误(Error)。 - -**1、运行时异常** - -定义:RuntimeException及其子类都被称为运行时异常。 - -特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fast机制产生的ConcurrentModificationException异常(java.util包下面的所有的集合类都是快速失败的,“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制,这个错叫并发修改异常。Fail-safe,java.util.concurrent包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。)等,都属于运行时异常。 - -常见的五种运行时异常: - -ClassCastException(类转换异常) - -IndexOutOfBoundsException(数组越界) - -NullPointerException(空指针异常) - -ArrayStoreException(数据存储异常,操作数组是类型不一致) - -BufferOverflowException - - - -**2、被检查异常** - -定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。 - -特点 : Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。 -如: - -IOException - -FileNotFoundException - -SQLException - -被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的``FileNotFoundException``。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的``NullPointerException``。 - -**3、错误** - -定义 : Error类及其子类。 - -特点 : 和运行时异常一样,编译器也不会对错误进行检查。 - -当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。OutOfMemoryError、ThreadDeath。 - -Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等 - - -### 25、OOM你遇到过哪些情况,SOF你遇到过哪些情况 - -**OOM**: - -1,OutOfMemoryError异常 - -除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。 - -Java Heap 溢出: - -一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。 - -java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。 - -出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。 - -如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 - -如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。 - -2,虚拟机栈和本地方法栈溢出 - -如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 - -如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常 - -这里需要注意当栈的大小越大可分配的线程数就越少。 - -3,运行时常量池溢出 - -异常信息:java.lang.OutOfMemoryError:PermGenspace - -如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。 - -4,方法区溢出 - -方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。 - -异常信息:java.lang.OutOfMemoryError:PermGenspace - -方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。 - - -**SOF(堆栈溢出StackOverflow):** - -StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。 - -因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。 - -栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。 - -### 26、 简述线程、程序、进程的基本概念。以及他们之间关系是什么? - -**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - - - -### 27、线程有哪些基本状态? - - Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4节)。 -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190618162826310.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zOTk0MDIwNg==,size_16,color_FFFFFF,t_70) -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4节): - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190618162853995.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zOTk0MDIwNg==,size_16,color_FFFFFF,t_70) - - 操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。 - -> 操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。 - - - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190618163143146.png) - - 当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 TERMINATED(终止) 状态。 - - - -### 28、Java 序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用 transient 关键字修饰。 - -transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。 - - - -### 29、Java 中 IO 流 - -**Java 中 IO 流分为几种?** - -- 按照流的流向分,可以分为输入流和输出流; -- 按照操作单元划分,可以划分为字节流和字符流; -- 按照流的角色划分为节点流和处理流。 - -Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 - -- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 -- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -按操作方式分类结构图: - - ![IO-操作方式分类](https://camo.githubusercontent.com/639ec442b39898de071c3e4fd098215fb48f11e9/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d362f494f2d2545362539332538442545342542442539432545362539362542392545352542432538462545352538382538362545372542312542422e706e67) - -按操作对象分类结构图: - -[![IO-操作对象分类](https://camo.githubusercontent.com/4a44e49ab13eacac26cbb0e481db73d6d11181b7/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d362f494f2d2545362539332538442545342542442539432545352541462542392545382542312541312545352538382538362545372542312542422e706e67)](https://camo.githubusercontent.com/4a44e49ab13eacac26cbb0e481db73d6d11181b7/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d362f494f2d2545362539332538442545342542442539432545352541462542392545382542312541312545352538382538362545372542312542422e706e67) - - - - - -### 30、 Java IO与 NIO的区别 - -推荐阅读:https://mp.weixin.qq.com/s/N1ojvByYmary65B6JM1ZWA - - -### 31、java反射的作用于原理 - -**1、定义:** - - 反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。 - -> **这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。** - - - -**2、哪里会用到反射机制?** - -jdbc就是典型的反射 - -```java -Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类 -``` - - 这就是反射。如hibernate,struts等框架使用反射实现的。 - - - -**3、反射的实现方式:** - - 第一步:获取Class对象,有4中方法: -1)Class.forName(“类的路径”); -2)类名.class -3)对象名.getClass() -4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象 - - - -**4、实现Java反射的类:** - -1)Class:表示正在运行的Java应用程序中的类和接口 -注意: 所有获取对象的信息都需要Class类来实现。 -2)Field:提供有关类和接口的属性信息,以及对它的动态访问权限。 -3)Constructor:提供关于类的单个构造方法的信息以及它的访问权限 -4)Method:提供类或接口中某个方法的信息 - -**5、反射机制的优缺点:** - -**优点:** -1)能够运行时动态获取类的实例,提高灵活性; -2)与动态编译结合 -**缺点:** -1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。 -解决方案: -1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度; -2、多次创建一个类的实例时,有缓存会快很多 -3、ReflectASM工具类,通过字节码生成的方式加快反射速度 -2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性) - -### 32、说说List,Set,Map三者的区别? - -- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 -- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。 -- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。 - - - -## JVM篇 - - - -### 1、知识点汇总 - - JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高. - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190529100157416.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hvbmd6aGVuOTE=,size_14,color_FFFFFF,t_70) - - - -其中内存模型,类加载机制,GC是重点方面.性能调优部分更偏向应用,重点突出实践能力.编译器优化和执行模式部分偏向于理论基础,重点掌握知识点. - -需了解 -**内存模型**各部分作用,保存哪些数据. - -**类加载**双亲委派加载机制,常用加载器分别加载哪种类型的类. - -**GC**分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景. - -**性能调优**常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法. - -**执行模式**解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景,C2针对的是server模式,优化更激进.新技术方面Java10的graal编译器 - -**编译器优化j**avac的编译过程,ast抽象语法树,编译器优化和运行器优化. - - - -### 2、知识点详解: - -**1、JVM内存模型:** - -线程独占:栈,本地方法栈,程序计数器 -线程共享:堆,方法区 - -**2、栈:** - -又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法出口等信息.调用方法时执行入栈,方法返回式执行出栈. - -**3、本地方法栈** - -与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈. - -**4、程序计数器** - -保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行Native方法时,程序计数器为空. - -**5、堆** - -JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆没有可用空间时,会抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理 - -**6、方法区:** - -又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7的永久代和1.8的元空间都是方法区的一种实现 - - - -**7、JVM 内存可见性** - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190529100559473.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hvbmd6aGVuOTE=,size_12,color_FFFFFF,t_70) - -JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证. - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190529100616206.png) - - - -### 3、类加载与卸载 - -**加载过程** - - ![img](https://img-blog.csdnimg.cn/20190529100641220.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hvbmd6aGVuOTE=,size_12,color_FFFFFF,t_70) - -其中**验证,准备,解析**合称链接 - -**加载**通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象. - -**验证**确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全. - -**准备**进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态变量,因为final变量在编译时分配. - -**解析**将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量等. - -**初始化**主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才会初始化. - -触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,或者某个子类初始化的时候. - -Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸. - - - **1、加载机制-双亲委派模式** - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/2019052910075352.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hvbmd6aGVuOTE=,size_10,color_FFFFFF,t_70) - - - -双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器.父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.* - -**优点:** - -1. 避免类的重复加载 -2. 避免Java的核心API被篡改 - - - -**2、分代回收** - - 分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间. -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190529100824512.png) - - 年轻代->标记-复制 -老年代->标记-清除 - -**3、回收算法** - -*a、G1算法* - -1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全部的增量式清理,以保证停顿时间不会过长 - -其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域(region),一部分用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区. - -同CMS相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间. - -年轻代回收: -并行复制采用复制算法,并行收集,会StopTheWorld. - -老年代回收: -会对年轻代一并回收 - -初始标记完成堆root对象的标记,会StopTheWorld. -并发标记 GC线程和应用线程并发执行. -最终标记完成三色标记周期,会StopTheWorld. -复制/清楚会优先对可回收空间加大的区域进行回收 - -*b、ZGC算法* - -前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿时间. - - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190529101222648.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hvbmd6aGVuOTE=,size_10,color_FFFFFF,t_70) - -- 着色指针 -- 读屏障 -- 并发处理 -- 基于region -- 内存压缩(整理) - -roots标记:标记root对象,会StopTheWorld. -并发标记:利用读屏障与应用线程一起运行标记,可能会发生StopTheWorld. -清除会清理标记为不可用的对象. -roots重定位:是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生.重定位最开始会StopTheWorld,却决于重定位集与对象总活动集的比例. -并发重定位与并发标记类似. - - - -### 4、简述一下JVM的内存模型 - -**1.JVM内存模型简介** - -JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一些区域的数据是线程性独立的,随着线程创建和销毁。jvm内存模型总体架构图如下:(摘自oracle[官方网站](https://link.jianshu.com?t=http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html)) - - ![img](https://upload-images.jianshu.io/upload_images/5321750-7153edcd60982fbb.png?imageMogr2/auto-orient/strip|imageView2/2/w/960/format/webp) - - JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个JVM内存模型架构图: - - ![img](https://upload-images.jianshu.io/upload_images/5321750-f75d9f191907ddda.png?imageMogr2/auto-orient/strip|imageView2/2/w/746/format/webp) JVM内存分为线程私有区和线程共享区 - - - -##### 线程私有区 - -**1、程序计数器** - -当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。 - -**2、虚拟机栈** - -线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。 - 使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图: - - ![img](https://upload-images.jianshu.io/upload_images/5321750-aa60ae3b9225d09f.png?imageMogr2/auto-orient/strip|imageView2/2/w/606/format/webp) - - - -**3、本地方法栈** - - 与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。 - - - -##### 线程共享区 - -**1、方法区** - -线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收) - - - -**2、堆** - -存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则OutOfMemoryError。 - - ![img](https://upload-images.jianshu.io/upload_images/5321750-9e4c27ecdfee3d58.png?imageMogr2/auto-orient/strip|imageView2/2/w/573/format/webp) - -``` -Young Generation 即图中的Eden + From Space(s0) + To Space(s1) -Eden 存放新生的对象 -Survivor Space 有两个,存放每次垃圾回收后存活的对象(s0+s1) -Old Generation Tenured Generation 即图中的Old Space - 主要存放应用程序中生命周期长的存活对象 - -``` - - - -### 5、堆和栈的区别 - - 栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。 - -**1、功能不同** - - 栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。 - -**2、共享性不同** - - 栈内存是线程私有的。 -堆内存是所有线程共有的。 - -**3、异常错误不同** - - 如果栈内存或者堆内存不足都会抛出异常。 -栈空间不足:java.lang.StackOverFlowError。 -堆空间不足:java.lang.OutOfMemoryError。 - -**4、空间大小** - - 栈的空间大小远远小于堆的。 - - - -### 6、什么时候会触发FullGC - -除直接调用System.gc外,触发Full GC执行的情况有如下四种。 -**1. 旧生代空间不足** -旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: -java.lang.OutOfMemoryError: Java heap space -为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 - -**2. Permanet Generation空间满** -PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: -java.lang.OutOfMemoryError: PermGen space -为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 - -**3. CMS GC时出现promotion failed和concurrent mode failure** -对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。 -promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 -应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。 - - - -**4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间** -这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 -例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 -当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 -除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。 - - - -### 7、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”? - - Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。 - - - -### 8、Java内存结构 - - ![《24个Jvm面试题总结及答案》](https://camo.githubusercontent.com/3d08568d6d5e8bf0b13e737e99a68206fa724808/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f3030327a6f347a61643369393770366935617878313473782f696d6167655f31626c39743671366c6162306f6b6f666f7631756d7637637431332e706e67) - -方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。 - -- Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 -- 方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 -- 程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。 -- JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 -- 本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。 - - - -### 9、对象分配规则 - -- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。 -- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。 -- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。 -- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 -- 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。 - - - -### 10、描述一下JVM加载class文件的原理机制? - -JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明: - -- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar); -- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap; -- System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。 - - - -### 11、Java对象创建过程 - -1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲) - -2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)” - -3.将除对象头外的对象内存空间初始化为0 - -4.对对象头进行必要设置 - - - -### 12、类的生命周期 - - 类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图; - - ![《24个Jvm面试题总结及答案》](https://camo.githubusercontent.com/e2722f0655be7b02c6f8b128e3675c14e9c0c013/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f737936663436726165787437676c6179673832776274786e2f696d6167655f31626c39743167656731356c6a35387034316b643135316b3639392e706e67) - -- 加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象 -- 连接,连接又包含三块内容:验证、准备、初始化。 1)验证,文件格式、元数据、字节码、符号引用验证; 2)准备,为类的静态变量分配内存,并将其初始化为默认值; 3)解析,把类中的符号引用转换为直接引用 -- 初始化,为类的静态变量赋予正确的初始值 -- 使用,new出对象程序中使用 -- 卸载,执行垃圾回收 - -### 13、简述Java的对象结构 - -Java对象由三个部分组成:对象头、实例数据、对齐填充。 - -对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。 - -实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的) - -对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐) - - - -### 14、如何判断对象可以被回收? - -判断对象是否存活一般有两种方式: - -- 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。 -- 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。 - - - -### 15、JVM的永久代中会发生垃圾回收么? - - 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区) - - - -### 16、垃圾收集算法 - -GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。 - -- 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 -- 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 -- 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 -- 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 - - - -### 17、调优命令有哪些? - -Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo - -- jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 -- jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 -- jmap,JVM Memory Map命令用于生成heap dump文件 -- jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看 -- jstack,用于生成java虚拟机当前时刻的线程快照。 -- jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。 - - - -### 18、调优工具 - -常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。 - -- jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控 -- jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。 -- MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗 -- GChisto,一款专业分析gc日志的工具 - - - -### 19、Minor GC与Full GC分别在什么时候发生? - - 新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC - - - -### 20、你知道哪些JVM性能调优 - -- 设定堆内存大小 - --Xmx:堆内存最大限制。 - -- 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代 - --XX:NewSize:新生代大小 - --XX:NewRatio 新生代和老生代占比 - --XX:SurvivorRatio:伊甸园空间和幸存者空间的占比 - -- 设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC - - - -### 21、JVM内存分哪几个区,每个区的作用是什么? - -Java虚拟机主要分为以下一个区: - -* 方法区: - 1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类 型的卸载 - 2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。 - 3. 该区域是被线程共享的。 - 4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编 译时确定,运行时生成的常量也会存在这个常量池中。 -* 虚拟机栈: - 1. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操 作数栈、动态链接和方法出口等信息。 - 2. 虚拟机栈是线程私有的,它的生命周期与线程相同。 - 3. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指 向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定 - 4. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式 - 5. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。 -* 本地方法栈 - * 本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。 -* 堆 - * Java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作 。 -* 程序计数器 - * 内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。 - -### 22、简述Java垃圾回收机制? - -在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。 - - - -### 23、什么是类加载器,类加载器有哪些? - -实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。 - -主要有一下四种类加载器: - -1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。 -2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 -3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 -4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。 - - - -### 24、你有没有遇到过OutOfMemory问题?你是怎么来处理这个问题的?处理 过程中有哪些收获? - -常见的原因 - -* 内存加载的数据量太大:一次性从数据库取太多数据; -* 集合类中有对对象的引用,使用后未清空,GC不能进行回收; -* 代码中存在循环产生过多的重复对象; -* 启动参数堆内存值小。 - -### 25、JDK 1.8之后Perm Space有哪些变动? MetaSpace⼤⼩默认是⽆限的么? 还是你们会通过什么⽅式来指定⼤⼩? - -JDK 1.8后用元空间替代了 Perm Space;字符串常量存放到堆内存中。 - -MetaSpace大小默认没有限制,一般根据系统内存的大小。JVM会动态改变此值。 - --XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小(Oracle逻辑存储上的初始高水位,the initial high-water-mark)。此值为估计值,MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。 --XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。 - - - -### 26、跟JVM内存相关的几个核心参数图解 - -![](https://user-gold-cdn.xitu.io/2020/1/16/16fad2ca79ea1cb7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - - - - -### 27、如何启动系统的时候设置JVM的启动参数 - -![](https://user-gold-cdn.xitu.io/2020/1/16/16fad2da823043f8?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - - -## 多线程&并发篇 - - - -### 1、Java中实现多线程有几种方法 - -继承Thread类; -实现Runnable接口; -实现Callable接口通过FutureTask包装器来创建Thread线程; -使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三种方式)。 - - - -### 2、如何停止一个正在运行的线程 - -1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 - -2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 - -3、使用interrupt方法中断线程。 - -```java -class MyThread extends Thread { - volatile boolean stop = false; - - public void run() { - while (!stop) { - System.out.println(getName() + " is running"); - try { - sleep(1000); - } catch (InterruptedException e) { - System.out.println("week up from blcok..."); - stop = true; // 在异常处理代码中修改共享变量的状态 - } - } - System.out.println(getName() + " is exiting..."); - } -} - -class InterruptThreadDemo3 { - public static void main(String[] args) throws InterruptedException { - MyThread m1 = new MyThread(); - System.out.println("Starting thread..."); - m1.start(); - Thread.sleep(3000); - System.out.println("Interrupt thread...: " + m1.getName()); - m1.stop = true; // 设置共享变量为true - m1.interrupt(); // 阻塞时退出阻塞状态 - Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况 - System.out.println("Stopping application..."); - } -} -``` - - - - - -### 3、notify()和notifyAll()有什么区别? - -notify可能会导致死锁,而notifyAll则不会 - -任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码 - -使用notifyall,可以唤醒 - 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。 - -wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。 - -notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中. - - - -### 4、sleep()和wait() 有什么区别? - -对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。 - -sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。 - -当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。 - - - -### 5、volatile 是什么?可以保证有序性吗? - -一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: - -1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。 - -2)禁止进行指令重排序。 - -volatile 不是原子性操作 - -什么叫保证部分有序性? - -当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行; - -```java -x = 2; //语句1 -y = 0; //语句2 -flag = true; //语句3 -x = 4; //语句4 -y = -1; //语句5 -``` - -由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。 - -使用 Volatile 一般用于 状态标记量 和 单例模式的双检锁 - - - -### 6、Thread 类中的start() 和 run() 方法有什么区别? - -start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。 - - - -### 7、为什么wait, notify 和 notifyAll这些方法不在thread类里面? - -明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。 - - - -### 8、为什么wait和notify方法要在同步块中调用? - -1. 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。 -2. 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。 -3. 还有一个原因是为了避免wait和notify之间产生竞态条件。 - -wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。 - -在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。 - -调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:"特殊状态已经被设置"。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。 - -### 9、Java中interrupted 和 isInterruptedd方法的区别? - -interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。 - - - -### 10、Java中synchronized 和 ReentrantLock 有什么不同? - -相似点: - -这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的. - -区别: - -这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。 - -Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。 - -由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项: - -1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。 - -2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。 - -3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。 - - - -### 11、有三个线程T1,T2,T3,如何保证顺序执行? - -在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。 - -实际上先启动三个线程中哪一个都行, -因为在每个线程的run方法中用join方法限定了三个线程的执行顺序。 - -```java -public class JoinTest2 { - - // 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行 - - public static void main(String[] args) { - - final Thread t1 = new Thread(new Runnable() { - - @Override - public void run() { - System.out.println("t1"); - } - }); - final Thread t2 = new Thread(new Runnable() { - - @Override - public void run() { - try { - // 引用t1线程,等待t1线程执行完 - t1.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("t2"); - } - }); - Thread t3 = new Thread(new Runnable() { - - @Override - public void run() { - try { - // 引用t2线程,等待t2线程执行完 - t2.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("t3"); - } - }); - t3.start();//这里三个线程的启动顺序可以任意,大家可以试下! - t2.start(); - t1.start(); - } -} -``` - - - -### **12、SynchronizedMap和ConcurrentHashMap有什么区别?** - -SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。 - -所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。 - - - -### 13、什么是线程安全 - -线程安全就是说多线程访问同一代码,不会产生不确定的结果。 - -在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。 - -线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。 - -如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 - - - -### 14、Thread类中的yield方法有什么作用? - -Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。 - - - -### 15、Java线程池中submit() 和 execute()方法有什么区别? - -两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。 - - - -### 16、说一说自己对于 synchronized 关键字的了解 - - synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - - -### 17、说说自己是怎么使用 synchronized 关键字,在项目中用到了吗synchronized关键字最主要的三种使用方式: -**修饰实例方法**: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -**修饰静态方法**: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。** -**修饰代码块**: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 -**总结**: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能! - - - -### 18、什么是线程安全?Vector是一个线程安全类吗? - -如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分 成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。 - - - -### 19、 **volatile关键字的作用?** - -一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: - -- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 -- 禁止进行指令重排序。 - -- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 -- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。 -- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。 -- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。 - -volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 - - - -### 20、常用的线程池有哪些? - -- newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 -- newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 -- newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 -- newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。 -- newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 - - - -### 21、简述一下你对线程池的理解 - -(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理利用线程池能够带来三个好处。 - -第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 - -第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - -第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - - - -### 22、Java程序是如何执行的 - -我们日常的工作中都使用开发工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的调试程序,或者是通过打包工具把项目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常运行了,但你有没有想过 Java 程序内部是如何执行的?其实不论是在开发工具中运行还是在 Tomcat 中运行,Java 程序的执行流程基本都是相同的,它的执行流程如下: - -- 先把 Java 代码编译成字节码,也就是把 .java 类型的文件编译成 .class 类型的文件。这个过程的大致执行流程:Java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字符码生成器 -> 最终生成字节码,其中任何一个节点执行失败就会造成编译失败; -- 把 class 文件放置到 Java 虚拟机,这个虚拟机通常指的是 Oracle 官方自带的 Hotspot JVM; -- Java 虚拟机使用类加载器(Class Loader)装载 class 文件; -- 类加载完成之后,会进行字节码效验,字节码效验通过之后 JVM 解释器会把字节码翻译成机器码交由操作系统执行。但不是所有代码都是解释执行的,JVM 对此做了优化,比如,以 Hotspot 虚拟机来说,它本身提供了 JIT(Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热点代码编译为机器码,这个时候字节码就变成了编译执行。Java 程序执行流程图如下: - - ![img](https://pics7.baidu.com/feed/9922720e0cf3d7caf8b227aecb7be60c6a63a90f.png?token=5bc0a1783459586fb4e21b13579950c9&s=B8A05D32150F65491865D0420300F0F1) - - - -### 23、说一说自己对于 synchronized 关键字的了解 - -synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 - -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - -### 24、说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 - -**synchronized关键字最主要的三种使用方式:** - -* **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** -* **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -* **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能! - -下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单利模式的原理呗!” - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} - -``` - -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - -### 25、 讲一下 synchronized 关键字的底层原理 - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -复制代码 -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized 关键字原理](https://user-gold-cdn.xitu.io/2018/10/26/166add616a292bcf?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized 关键字原理](https://user-gold-cdn.xitu.io/2018/10/26/166add6169fc206d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - - -### 26、 为什么要用线程池? - -线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处: - -* **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -* **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -* **提高线程的可管理性。** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 27、 实现Runnable接口和Callable接口的区别 - -如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。 - -**备注:** 工具类`Executors`可以实现`Runnable`对象和`Callable`对象之间的相互转换。(`Executors.callable(Runnable task)`或`Executors.callable(Runnable task,Object resule)`)。 - -### 28、 执行execute()方法和submit()方法的区别是什么呢? - -1)**`execute()` 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** - -2)**submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功**,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -### 29、 如何创建线程池 - -《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险** - -> Executors 返回线程池对象的弊端如下: -> -> * **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。 -> * **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -**方式一:通过构造方法实现** - -![通过构造方法实现](https://user-gold-cdn.xitu.io/2018/10/30/166c4a5baac923e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -**方式二:通过Executor 框架的工具类Executors来实现** 我们可以创建三种类型的ThreadPoolExecutor: - -* **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -* **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -* **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 - -对应Executors工具类中的方法如图所示: - -![通过Executor 框架的工具类Executors来实现](https://user-gold-cdn.xitu.io/2018/10/30/166c4a5baa9ca5e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - - -## Spring篇 - -推荐阅读: [极客学院Spring Wiki](http://wiki.jikexueyuan.com/project/spring/transaction-management.html) - - - -### 1、 Spring的IOC和AOP机制? - -我们是在使用Spring框架的过程中,其实就是为了使用IOC,依赖注入,和AOP,面向切面编程,这两个是Spring的灵魂。 - -主要用到的设计模式有工厂模式和代理模式。 - -IOC就是典型的工厂模式,通过sessionfactory去注入实例。 - -AOP就是典型的代理模式的体现。 - -代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。 - -**spring的IoC容器是spring的核心,spring AOP是spring框架的重要组成部分。** - -*在传统的程序设计中,当调用者需要被调用者的协助时,通常由调用者来创建被调用者的实例。但在spring里创建被调用者的工作不再由调用者来完成,因此控制反转(IoC);创建被调用者实例的工作通常由spring容器来完成,然后注入调用者,因此也被称为依赖注入(DI),依赖注入和控制反转是同一个概念。* - -面向方面编程(AOP)是以另一个角度来考虑程序结构,通过分析程序结构的关注点来完善面向对象编程(OOP)。OOP将应用程序分解成各个层次的对象,而AOP将程序分解成多个切面。spring AOP 只实现了方法级别的连接点,在J2EE应用中,AOP拦截到方法级别的操作就已经足够。在spring中,未来使IoC方便地使用健壮、灵活的企业服务,需要利用spring AOP实现为IoC和企业服务之间建立联系。 - -**IOC:控制反转也叫依赖注入。利用了工厂模式** -**将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类(假设这个类名是A),分配的方法就是调用A的setter方法来注入,而不需要你在A里面new这些bean了。** -**注意:面试的时候,如果有条件,画图,这样更加显得你懂了.** - -spring ioc初始化流程 - - ![Spring IOC的初始化过程](https://camo.githubusercontent.com/3b07a520440ff631990c027c2437d131fba25efe/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f352f32322f313633383739303365653732633833313f773d37303926683d353626663d706e6726733d34363733) - - - -**AOP:面向切面编程。(Aspect-Oriented Programming)** -**AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。** -**将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。** - -**实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码.** - -简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用aop思想来做.你先写个类写个类方法,方法经实现打印‘你好’,然后Ioc这个类 ref=“biz.*”让每个类都注入即可实现。 - - - -### 2、 Spring中Autowired和Resource关键字的区别? - -@Resource和@Autowired都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。 - -1、共同点 - -两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。 - -2、不同点 - -(1)@Autowired - -@Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired;只按照byType注入。 - -```java -public class TestServiceImpl { - // 下面两种@Autowired只要使用一种即可 - @Autowired - private UserDao userDao; // 用于字段上 - - @Autowired - public void setUserDao(UserDao userDao) { // 用于属性的方法上 - this.userDao = userDao; - } -} -``` - - @Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用。如下: - -```java -public class TestServiceImpl { - @Autowired - @Qualifier("userDao") - private UserDao userDao; -} -``` - -(2)@Resource - -@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。 - -```java -public class TestServiceImpl { - // 下面两种@Resource只要使用一种即可 - @Resource(name="userDao") - private UserDao userDao; // 用于字段上 - - @Resource(name="userDao") - public void setUserDao(UserDao userDao) { // 用于属性的setter方法上 - this.userDao = userDao; - } -} -``` - -注:最好是将@Resource放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。 - -@Resource装配顺序: - -①如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。 - -②如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。 - -③如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。 - -④如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。 - -@Resource的作用相当于@Autowired,只不过@Autowired按照byType自动注入。 - - - -### 3、依赖注入的方式有几种,各是什么? - -**一、构造器注入** -将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。 - -优点: -对象初始化完成后便可获得可使用的对象。 - -缺点: -当需要注入的对象很多时,构造器参数列表将会很长; -不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数,麻烦。 - -**二、setter方法注入** -IoC Service Provider通过调用成员变量提供的setter函数将被依赖对象注入给依赖类。 - -优点: -灵活。可以选择性地注入需要的对象。 - -缺点: -依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。 - -**三、接口注入** -依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象。 - -优点 -接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。 - -缺点: -侵入行太强,不建议使用。 - -PS:什么是侵入行? -如果类A要使用别人提供的一个功能,若为了使用这功能,需要在自己的类中增加额外的代码,这就是侵入性。 - - - -### 4、讲一下什么是Spring - -Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于XML的配置、基于注解的配置、基于Java的配置。 - -主要由以下几个模块组成: - -Spring Core:核心类库,提供IOC服务; - -Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等); - -Spring AOP:AOP服务; - -Spring DAO:对JDBC的抽象,简化了数据访问异常的处理; - -Spring ORM:对现有的ORM框架的支持; - -Spring Web:提供了基本的面向Web的综合特性,例如多方文件上传; - -Spring MVC:提供面向Web应用的Model-View-Controller实现。 - - - - -### 5、Spring MVC流程 - - - -工作原理: - -![img](https://images2015.cnblogs.com/blog/249993/201612/249993-20161212142542042-2117679195.jpg) - -1、 用户发送请求至前端控制器DispatcherServlet。 - -2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。 - -3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。 - -4、 DispatcherServlet调用HandlerAdapter处理器适配器。 - -5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。 - -6、 Controller执行完成返回ModelAndView。 - -7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。 - -8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。 - -9、 ViewReslover解析后返回具体View。 - -10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。 - -11、 DispatcherServlet响应用户。 - - - -**组件说明:** - -以下组件通常使用框架提供实现: - -DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。 - -HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 - -HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。 - -ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jsp、freemarker、pdf、excel等。 - -**组件:** -**1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供** -作用:接收请求,响应结果,相当于转发器,中央处理器。有了dispatcherServlet减少了其它组件之间的耦合度。 -用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。 - -**2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供** -作用:根据请求的url查找Handler -HandlerMapping负责根据用户请求找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 - -**3、处理器适配器HandlerAdapter** -作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler -通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。 - -**4、处理器Handler(需要工程师开发)** -**注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler** -Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 -由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。 - -**5、视图解析器View resolver(不需要工程师开发),由框架提供** -作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) -View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。 -一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。 - -**6、视图View(需要工程师开发jsp...)** -View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...) - -**核心架构的具体流程步骤如下:** -1、首先用户发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制; -2、DispatcherServlet——>HandlerMapping, HandlerMapping 将会把请求映射为HandlerExecutionChain 对象(包含一个Handler 处理器(页面控制器)对象、多个HandlerInterceptor 拦截器)对象,通过这种策略模式,很容易添加新的映射策略; -3、DispatcherServlet——>HandlerAdapter,HandlerAdapter 将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器; -4、HandlerAdapter——>处理器功能处理方法的调用,HandlerAdapter 将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView 对象(包含模型数据、逻辑视图名); -5、ModelAndView的逻辑视图名——> ViewResolver, ViewResolver 将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术; -6、View——>渲染,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术; -7、返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束。 - -下边两个组件通常情况下需要开发: - -Handler:处理器,即后端控制器用controller表示。 - -View:视图,即展示给用户的界面,视图中通常需要标签语言展示模型数据。 - - - -**在讲SpringMVC之前我们先来看一下什么是MVC模式** - -MVC:MVC是一种设计模式 - -MVC的原理图: - - ![img](https://images2015.cnblogs.com/blog/249993/201702/249993-20170207135959401-404841652.png) - -**分析:** - -M-Model 模型(完成业务逻辑:有javaBean构成,service+dao+entity) - -V-View 视图(做界面的展示 jsp,html……) - -C-Controller 控制器(接收请求—>调用模型—>根据结果派发页面) - - - -**springMVC是什么:** - -  springMVC是一个MVC的开源框架,springMVC=struts2+spring,springMVC就相当于是Struts2加上sring的整合,但是这里有一个疑惑就是,springMVC和spring是什么样的关系呢?这个在百度百科上有一个很好的解释:意思是说,springMVC是spring的一个后续产品,其实就是spring在原有基础上,又提供了web应用的MVC模块,可以简单的把springMVC理解为是spring的一个模块(类似AOP,IOC这样的模块),网络上经常会说springMVC和spring无缝集成,其实springMVC就是spring的一个子模块,所以根本不需要同spring进行整合。 - -**SpringMVC的原理图:** - -**![img](https://images2015.cnblogs.com/blog/249993/201702/249993-20170207140151791-1932120070.png)** - -**看到这个图大家可能会有很多的疑惑,现在我们来看一下这个图的步骤:(可以对比MVC的原理图进行理解)** - -第一步:用户发起请求到前端控制器(DispatcherServlet) - -第二步:前端控制器请求处理器映射器(HandlerMappering)去查找处理器(Handle):通过xml配置或者注解进行查找 - -第三步:找到以后处理器映射器(HandlerMappering)像前端控制器返回执行链(HandlerExecutionChain) - -第四步:前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)去执行处理器(Handler) - -第五步:处理器适配器去执行Handler - -第六步:Handler执行完给处理器适配器返回ModelAndView - -第七步:处理器适配器向前端控制器返回ModelAndView - -第八步:前端控制器请求视图解析器(ViewResolver)去进行视图解析 - -第九步:视图解析器像前端控制器返回View - -第十步:前端控制器对视图进行渲染 - -第十一步:前端控制器向用户响应结果 - -**看到这些步骤我相信大家很感觉非常的乱,这是正常的,但是这里主要是要大家理解springMVC中的几个组件:** - -前端控制器(DispatcherServlet):接收请求,响应结果,相当于电脑的CPU。 - -处理器映射器(HandlerMapping):根据URL去查找处理器 - -处理器(Handler):(需要程序员去写代码处理逻辑的) - -处理器适配器(HandlerAdapter):会把处理器包装成适配器,这样就可以支持多种类型的处理器,类比笔记本的适配器(适配器模式的应用) - -视图解析器(ViewResovler):进行视图解析,多返回的字符串,进行处理,可以解析成对应的页面 - - - -### 6、SpringMVC怎么样设定重定向和转发的? - -(1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" - -(2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" - - - -### 7、 **SpringMVC常用的注解有哪些?** - -@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 - -@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 - -@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 - - - -### 8、 **Spring的AOP理解:** - -OOP面向对象,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。 - -AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。 - -AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。 - -(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。 - -(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。 - -Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理: - - ①JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。 - - ②如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 - -(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。 - -### 9、Spring的IOC理解 - -(1)IOC就是控制反转,是指创建对象的控制权的转移,以前创建对象的主动权和时机是由自己把控的,而现在这种权力转移到Spring容器中,并由容器根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部资源。 - -(2)最直观的表达就是,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。 - -(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。 - -> IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。 - - - -### 10、解释一下spring bean的生命周期 - -首先说一下Servlet的生命周期:实例化,初始init,接收请求service,销毁destroy; - - Spring上下文中的Bean生命周期也类似,如下: - -(1)实例化Bean: - -对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。 - -(2)设置对象属性(依赖注入): - -实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。 - -(3)处理Aware接口: - -接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean: - -①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值; - -②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。 - -③如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文; - -(4)BeanPostProcessor: - -如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。 - -(5)InitializingBean 与 init-method: - -如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。 - -(6)如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术; - -> 以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。 - -(7)DisposableBean: - -当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法; - -(8)destroy-method: - -最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。 - - -### 11、 **解释Spring支持的几种bean的作用域。** - -Spring容器中的bean可以分为5个范围: - -(1)singleton:默认,每个容器中只有一个bean的实例,单例的模式由BeanFactory自身来维护。 - -(2)prototype:为每一个bean请求提供一个实例。 - -(3)request:为每一个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。 - -(4)session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。 - -(5)global-session:全局作用域,global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。 - - -### 12、 **Spring基于xml注入bean的几种方式:** - -(1)Set方法注入; - -(2)构造器注入:①通过index设置参数的位置;②通过type设置参数类型; - -(3)静态工厂注入; - -(4)实例工厂; - -详细内容可以阅读:https://blog.csdn.net/a745233700/article/details/89307518 - -### 13、Spring框架中都用到了哪些设计模式? - -(1)工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例; - -(2)单例模式:Bean默认为单例模式。 - -(3)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术; - -(4)模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。 - -(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现--ApplicationListener。 - - - - -## MyBatis篇 - -### 1、什么是MyBatis - -(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。 - -(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 - -(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 - - - - -### 2、MyBatis的优点和缺点 - -**优点:** - -(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 - -(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接; - -(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。 - -(4)能够与Spring很好的集成; - -(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。 - -**缺点** - -(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。 - -(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 - - - -### 3、#{}和${}的区别是什么? - -\#{}是预编译处理,${}是字符串替换。 - -Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; - -Mybatis在处理${}时,就是把${}替换成变量的值。 - -使用#{}可以有效的防止SQL注入,提高系统安全性。 - -### 4、当实体类中的属性名和表中的字段名不一样 ,怎么办 ? - - 第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 - -```xml -     -``` - - 第2种: 通过来映射字段名和实体类属性名的一一对应的关系。 - -```xml - - - - - - - - - - -``` - - - -### 5、Mybatis是如何进行分页的?分页插件的原理是什么? - -Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。 - -分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。 - - - -### 6、Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? - - - -第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。 - -第二种是使用sql列的别名功能,将列的别名书写为对象属性名。 - -有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。 - -### 7、 如何执行批量插入? - - 首先,创建一个简单的insert语句: - -```java - -     insert into names (name) values (#{value}) - -``` - - 然后在java代码中像下面这样执行批处理插入: - -```java - list names = new arraylist(); - names.add(“fred”); - names.add(“barney”); - names.add(“betty”); - names.add(“wilma”); - - // 注意这里 executortype.batch - sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch); - try { - namemapper mapper = sqlsession.getmapper(namemapper.class); - for (string name : names) { - mapper.insertname(name); - } - sqlsession.commit(); - }catch(Exception e){ - e.printStackTrace(); - sqlSession.rollback(); - throw e; -    } -     finally { -     sqlsession.close(); - } -``` - - - -### 8、Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签? - - ,加上动态sql的9个标签,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。 - - - -### 9、MyBatis实现一对一有几种方式?具体怎么操作的? - -有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; - -嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。 - - -### 10、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? - -Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 - -它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 - -当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 - -### 11、Mybatis的一级、二级缓存: - -1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。 - -2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ; - -3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。 - - - -## SpringBoot篇 - - - -### 1、什么是SpringBoot?为什么要用SpringBoot - -用来简化spring应用的初始搭建以及开发过程 使用特定的方式来进行配置(properties或yml文件) - -创建独立的spring引用程序 main方法运行 - -嵌入的Tomcat 无需部署war文件 - -简化maven配置 - -自动配置spring添加对应功能starter自动化配置 - -**spring boot来简化spring应用开发,约定大于配置,去繁从简,just run就能创建一个独立的,产品级别的应用** - - - -Spring Boot 优点非常多,如: - -一、独立运行 - -Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。 - -二、简化配置 - -spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。 -三、自动配置 - -Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。 - -四、无代码生成和XML配置 - -Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。 - -五、应用监控 - -Spring Boot提供一系列端点可以监控服务及应用,做健康检测。 - - - - -### 2、Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? - -启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: - -@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 - -@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 - -@ComponentScan:Spring组件扫描。 - - -### 3、运行Spring Boot有哪几种方式? - -1)打包用命令或者放到容器中运行 - -2)用 Maven/Gradle 插件运行 - -3)直接执行 main 方法运行 - - - -### 4、如何理解 Spring Boot 中的 Starters? - -Starters是什么: - -Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成Spring及其他技术,而不需要到处找示例代码和依赖包。如你想使用Spring JPA访问数据库,只要加入spring-boot-starter-data-jpa启动器依赖就能使用了。Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。 - -Starters命名: - -Spring Boot官方的启动器都是以spring-boot-starter-命名的,代表了一个特定的应用类型。第三方的启动器不能以spring-boot开头命名,它们都被Spring Boot官方保留。一般一个第三方的应该这样命名,像mybatis的mybatis-spring-boot-starter。 - -Starters分类: - - -1. Spring Boot应用类启动器 - -![img](https://img-blog.csdnimg.cn/20190314152153622.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0tldmluX0d1Ng==,size_16,color_FFFFFF,t_70) - -2. Spring Boot生产启动器 - -![img](https://img-blog.csdnimg.cn/20190314152237524.png) - -3. Spring Boot技术类启动器 - -![img](https://img-blog.csdnimg.cn/20190314152303517.png) - -4. 其他第三方启动器 - - - -### 5、 如何在Spring Boot启动的时候运行一些特定的代码? - -如果你想在Spring Boot启动的时候运行一些特定的代码,你可以实现接口**ApplicationRunner**或者**CommandLineRunner**,这两个接口实现方式一样,它们都只提供了一个run方法。 - -**CommandLineRunner**:启动获取命令行参数 - - - -### 6、 **Spring Boot 需要独立的容器运行吗?** - - 可以不需要,内置了 Tomcat/ Jetty 等容器。 - - - -### 7、 **Spring Boot中的监视器是什么?** - -Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态。 - - - -### 8、 **如何使用Spring Boot实现异常处理?** - - Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。 我们通过实现一个ControlerAdvice类,来处理控制器类抛出的所有异常。 - - - -### 9、 **你如何理解 Spring Boot 中的 Starters?** - - Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。 - - - -### 10、 **springboot常用的starter有哪些** - -spring-boot-starter-web 嵌入tomcat和web开发需要servlet与jsp支持 - -spring-boot-starter-data-jpa 数据库支持 - -spring-boot-starter-data-redis redis数据库支持 - -spring-boot-starter-data-solr solr支持 - -mybatis-spring-boot-starter 第三方的mybatis集成starter - - - -### 11、 **SpringBoot 实现热部署有哪几种方式?** - - 主要有两种方式: - -- Spring Loaded -- Spring-boot-devtools - - - -### 12、 **如何理解 Spring Boot 配置加载顺序?** - -在 Spring Boot 里面,可以使用以下几种方式来加载配置。 - - 1)properties文件; - - 2)YAML文件; - - 3)系统环境变量; - - 4)命令行参数; - - 等等…… - - - -### 13、 **Spring Boot 的核心配置文件有哪几个?它们的区别是什么?** - -pring Boot 的核心配置文件是 application 和 bootstrap 配置文件。 - -application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。 - -bootstrap 配置文件有以下几个应用场景。 - -- 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息; -- 一些固定的不能被覆盖的属性; -- 一些加密/解密的场景; - - - -### 14、如何集成 Spring Boot 和 ActiveMQ? - - 对于集成 Spring Boot 和 ActiveMQ,我们使用 -spring-boot-starter-activemq -依赖关系。 它只需要很少的配置,并且不需要样板代码。 - - - -### 15、如何重新加载Spring Boot上的更改,而无需重新启动服务器? - -这可以使用DEV工具来实现。通过这种依赖关系,您可以节省任何更改,嵌入式tomcat将重新启动。 - -Spring Boot有一个开发工具(DevTools)模块,它有助于提高开发人员的生产力。Java开发人员面临的一个主要挑战是将文件更改自动部署到服务器并自动重启服务器。 - -开发人员可以重新加载Spring Boot上的更改,而无需重新启动服务器。这将消除每次手动部署更改的需要。Spring Boot在发布它的第一个版本时没有这个功能。 - -这是开发人员最需要的功能。DevTools模块完全满足开发人员的需求。该模块将在生产环境中被禁用。它还提供H2数据库控制台以更好地测试应用程序。 - -org.springframework.boot - -spring-boot-devtools - -true - - - -### 16、 Spring Boot、Spring MVC 和 Spring 有什么区别? - - - -1、Spring - -Spring最重要的特征是依赖注入。所有 SpringModules 不是依赖注入就是 IOC 控制反转。 - -当我们恰当的使用 DI 或者是 IOC 的时候,我们可以开发松耦合应用。松耦合应用的单元测试可以很容易的进行。 - -2、Spring MVC - -Spring MVC 提供了一种分离式的方法来开发 Web 应用。通过运用像 DispatcherServelet,MoudlAndView 和 ViewResolver 等一些简单的概念,开发 Web 应用将会变的非常简单。 - -3、SpringBoot - -Spring 和 SpringMVC 的问题在于需要配置大量的参数。 - - ![img](https://mmbiz.qpic.cn/mmbiz_png/KLTiaLuJImELSYlz43K7eJXnVZcbNPt3h4P9rx8JicF7vYPt1sP52ibXeUqd5sibRSzDaMGM75r0M4ibOW0yIoSrRxA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - - Spring Boot 通过一个自动配置和启动的项来目解决这个问题。为了更快的构建产品就绪应用程序,Spring Boot 提供了一些非功能性特征。 - - - -### 17、 **能否举一个例子来解释更多 Staters 的内容?** - -让我们来思考一个 Stater 的例子 -Spring Boot Stater Web。 - -如果你想开发一个 web 应用程序或者是公开 REST 服务的应用程序。Spring Boot Start Web 是首选。让我们使用 Spring Initializr 创建一个 Spring Boot Start Web 的快速项目。 - -Spring Boot Start Web 的依赖项 - - ![img](https://mmbiz.qpic.cn/mmbiz_png/KLTiaLuJImELSYlz43K7eJXnVZcbNPt3hZtKfFL4LrGXbHEgRiafCzqq1E4s3x0iaj26fziavAkvLkLwKU5CLsM4icQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - - 下面的截图是添加进我们应用程序的不同的依赖项 - - ![img](https://mmbiz.qpic.cn/mmbiz_png/KLTiaLuJImELSYlz43K7eJXnVZcbNPt3hNKv6hhyicgxUSGYmxeRrPZX1rbvzomiaWNIZOHxEF3ibp6Kqm3eAwv8DQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - -依赖项可以被分为: - -- Spring - core,beans,context,aop -- Web MVC - (Spring MVC) -- Jackson - for JSON Binding -- Validation - Hibernate,Validation API -- Enbedded Servlet Container - Tomcat -- Logging - logback,slf4j - -任何经典的 Web 应用程序都会使用所有这些依赖项。Spring Boot Starter Web 预先打包了这些依赖项。 - -作为一个开发者,我不需要再担心这些依赖项和它们的兼容版本。 - -### 18、 **Spring Boot 还提供了其它的哪些 Starter Project Options?** - -Spring Boot 也提供了其它的启动器项目包括,包括用于开发特定类型应用程序的典型依赖项。 - -- spring-boot-starter-web-services - SOAP Web Services; -- spring-boot-starter-web - Web 和 RESTful 应用程序; -- spring-boot-starter-test - 单元测试和集成测试; -- spring-boot-starter-jdbc - 传统的 JDBC; -- spring-boot-starter-hateoas - 为服务添加 HATEOAS 功能; -- spring-boot-starter-security - 使用 SpringSecurity 进行身份验证和授权; -- spring-boot-starter-data-jpa - 带有 Hibeernate 的 Spring Data JPA; -- spring-boot-starter-data-rest - 使用 Spring Data REST 公布简单的 REST 服务; - - - -## MySQL篇 - -### 1、数据库的三范式是什么 - -第一范式:列不可再分 -第二范式:行可以唯一区分,主键约束 -第三范式:表的非主属性不能依赖与其他表的非主属性 外键约束 -且三大范式是一级一级依赖的,第二范式建立在第一范式上,第三范式建立第一第二范式上。 - - - -### 2、数据库引擎有哪些 - -如何查看mysql提供的所有存储引擎 - -``` -mysql> show engines; -``` - - ![查看MySQL提供的所有存储引擎](https://camo.githubusercontent.com/34b24ff87145208403dce01554932f05bbe3f167/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d362f6d7973716c2d656e67696e65732e706e67) - -mysql常用引擎包括:MYISAM、Innodb、Memory、MERGE - -- MYISAM:全表锁,拥有较高的执行速度,不支持事务,不支持外键,并发性能差,占用空间相对较小,对事务完整性没有要求,以select、insert为主的应用基本上可以使用这引擎 -- Innodb:行级锁,提供了具有提交、回滚和崩溃回复能力的事务安全,支持自动增长列,支持外键约束,并发能力强,占用空间是MYISAM的2.5倍,处理效率相对会差一些 -- Memory:全表锁,存储在内容中,速度快,但会占用和数据量成正比的内存空间且数据在mysql重启时会丢失,默认使用HASH索引,检索效率非常高,但不适用于精确查找,主要用于那些内容变化不频繁的代码表 -- MERGE:是一组MYISAM表的组合 - - -### 3、InnoDB与MyISAM的区别 - -1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务; -2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败; -3. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。 -4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快; -5. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高; - - - -**如何选择引擎?** - -如果没有特别的需求,使用默认的`Innodb`即可。 - -MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。 - -Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。 - - - -### 4、数据库的事务 - -**什么是事务?:** 多条sql语句,要么全部成功,要么全部失败。 - -**事务的特性:** - -**数据库事务特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、持久性(Durabiliy)。简称ACID。** - -- 原子性:组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作都成功,整个事务才会提交。任何一个操作失败,已经执行的任何操作都必须撤销,让数据库返回初始状态。 -- 一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的。即数据不会被破坏。如A转账100元给B,不管操作是否成功,A和B的账户总额是不变的。 -- 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对彼此产生干扰 -- 持久性:一旦事务提交成功,事务中的所有操作都必须持久化到数据库中。 - - - -### 5、索引问题 - -> 索引是对数据库表中一个或多个列的值进行排序的结构,建立索引有助于快速获取信息。 - -你也可以这样理解:索引就是加快检索表中数据的方法。数据库的索引类似于书籍的索引。在书籍中,索引允许用户不必翻阅完整个书就能迅速地找到所需要的信息。在数据库中,索引也允许数据库程序迅速地找到表中的数据,而不必扫描整个数据库。 - -`mysql`有4种不同的索引: - -- 主键索引(PRIMARY) - - 数据列不允许重复,不允许为NULL,一个表只能有一个主键。 - -- 唯一索引(UNIQUE) - - 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。 - - - 可以通过 `ALTER TABLE table_name ADD UNIQUE (column);` 创建唯一索引 - - 可以通过 `ALTER TABLE table_name ADD UNIQUE (column1,column2);` 创建唯一组合索引 - -- 普通索引(INDEX) - - - 可以通过`ALTER TABLE table_name ADD INDEX index_name (column);`创建普通索引 - - 可以通过`ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);`创建组合索引 - -- 全文索引(FULLTEXT) - - 可以通过`ALTER TABLE table_name ADD FULLTEXT (column);`创建全文索引 - - **索引并非是越多越好,创建索引也需要耗费资源,一是增加了数据库的存储空间,二是在插入和删除时要花费较多的时间维护索引** - -- 索引加快数据库的检索速度 -- 索引降低了插入、删除、修改等维护任务的速度 -- 唯一索引可以确保每一行数据的唯一性 -- 通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能 -- 索引需要占物理和数据空间 - - - -### 6、SQL优化 - -1、查询语句中不要使用select * - -2、尽量减少子查询,使用关联查询(left join,right join,inner join)替代 - -3、减少使用IN或者NOT IN ,使用exists,not exists或者关联查询语句替代 - -4、or 的查询尽量用 union或者union all 代替(在确认没有重复数据或者不用剔除重复数据时,union all会更好) - -5、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。 - -6、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num is null 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0 - - - -### 7、简单说一说drop、delete与truncate的区别 - -SQL中的drop、delete、truncate都表示删除,但是三者有一些差别 - -delete和truncate只删除表的数据不删除表的结构 -速度,一般来说: drop> truncate >delete -delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效; -如果有相应的trigger,执行的时候将被触发. truncate,drop是ddl, 操作立即生效,原数据不放到rollback segment中,不能回滚. 操作不触发trigger. - -### 8、什么是视图 - - 视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,试图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。 - - - -### 9、 什么是内联接、左外联接、右外联接? - -- 内联接(Inner Join):匹配2张表中相关联的记录。 -- 左外联接(Left Outer Join):除了匹配2张表中相关联的记录外,还会匹配左表中剩余的记录,右表中未匹配到的字段用NULL表示。 -- 右外联接(Right Outer Join):除了匹配2张表中相关联的记录外,还会匹配右表中剩余的记录,左表中未匹配到的字段用NULL表示。在判定左表和右表时,要根据表名出现在Outer Join的左右位置关系。 - - - -### 10、并发事务带来哪些问题? - -在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。 - -- **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。 -- **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。 -- **不可重复读(Unrepeatableread):** 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。 -- **幻读(Phantom read):** 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。 - -**不可重复读和幻读区别:** - -不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。 - - - -### 11、事务隔离级别有哪些?MySQL的默认隔离级别是? - -**SQL 标准定义了四个隔离级别:** - -- **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。 -- **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。 -- **REPEATABLE-READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。 -- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。 - - - -| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | -| ---------------- | ---- | ---------- | ------ | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看 - -``` -mysql> SELECT @@tx_isolation; -+-----------------+ -| @@tx_isolation | -+-----------------+ -| REPEATABLE-READ | -+-----------------+ -``` - -这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) 是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 **SERIALIZABLE(可串行化)** 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED(读取提交内容)** ,但是你要知道的是InnoDB 存储引擎默认使用 **REPEAaTABLE-READ(可重读)** 并不会有任何性能损失。 - -InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALIZABLE(可串行化)** 隔离级别。 - - - -### 12、大表如何优化? - -当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: - -#### 1. 限定数据的范围 - -务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内; - -#### 2. 读/写分离 - -经典的数据库拆分方案,主库负责写,从库负责读; - -#### 3. 垂直分区 - -**根据数据库里面数据表的相关性进行拆分。** 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。 - -**简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。** 如下图所示,这样来说大家应该就更容易理解了。 - -![1583307481617](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1583307481617.png) - -- **垂直拆分的优点:** 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。 -- **垂直拆分的缺点:** 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂; - -#### 4. 水平分区 - -**保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。** - -水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 - -![1583308353521](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1583308353521.png) - -水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 **水平拆分最好分库** 。 - -水平拆分能够 **支持非常大的数据量存储,应用端改造也少**,但 **分片事务难以解决** ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 **尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度** ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。 - -**下面补充一下数据库分片的两种常见方案:** - -- **客户端代理:** **分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。** 当当网的 **Sharding-JDBC** 、阿里的TDDL是两种比较常用的实现。 -- **中间件代理:** **在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。** 我们现在谈的 **Mycat** 、360的Atlas、网易的DDB等等都是这种架构的实现。 - -详细内容可以参考: MySQL大表优化方案: https://segmentfault.com/a/1190000006158186 - - - -### 13、分库分表之后,id 主键如何处理? - -因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。 - -生成全局 id 有下面这几种方式: - -- **UUID**:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。 -- **数据库自增 id** : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。 -- **利用 redis 生成 id :** 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。 -- **Twitter的snowflake算法** :Github 地址:https://github.com/twitter-archive/snowflake。 -- **美团的[Leaf](https://tech.meituan.com/2017/04/21/mt-leaf.html)分布式ID生成系统** :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html 。 - - - -### 14、mysql有关权限的表都有哪几个 - -MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容: - -- user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。 - -- db权限表:记录各个帐号在各个数据库上的操作权限。 - -- table_priv权限表:记录数据表级的操作权限。 - -- columns_priv权限表:记录数据列级的操作权限。 - -- host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。 - -### 15、mysql有哪些数据类型 - -**1、整数类型** ,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。 -长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。 -例子,假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。 - -**2、实数类型**,包括FLOAT、DOUBLE、DECIMAL。 -DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。 -而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。 -计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。 - -**3、字符串类型**,包括VARCHAR、CHAR、TEXT、BLOB -VARCHAR用于存储可变长字符串,它比定长类型更节省空间。 -VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。 -VARCHAR存储的内容超出设置的长度时,内容会被截断。 -CHAR是定长的,根据定义的字符串长度分配足够的空间。 -CHAR会根据需要使用空格进行填充方便比较。 -CHAR适合存储很短的字符串,或者所有值都接近同一个长度。 -CHAR存储的内容超出设置的长度时,内容同样会被截断。 - -**使用策略:** -对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。 -对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。 -使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。 -尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。 - -**4、枚举类型(ENUM)**,把不重复的数据存储为一个预定义的集合。 -有时可以使用ENUM代替常用的字符串类型。 -ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。 -ENUM在内部存储时,其实存的是整数。 -尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。 -排序是按照内部存储的整数 - -**5、日期和时间类型**,尽量使用timestamp,空间效率高于datetime, -用整数保存时间戳通常不方便处理。 -如果需要存储微妙,可以使用bigint存储。 -看到这里,这道真题是不是就比较容易回答了。 - - - -### 16、创建索引的三种方式,删除索引 - -第一种方式:在执行CREATE TABLE时创建索引 - -```sql -CREATE TABLE user_index2 ( - id INT auto_increment PRIMARY KEY, - first_name VARCHAR (16), - last_name VARCHAR (16), - id_card VARCHAR (18), - information text, - KEY name (first_name, last_name), - FULLTEXT KEY (information), - UNIQUE KEY (id_card) -); - -``` - - 第二种方式:使用ALTER TABLE命令去增加索引 - -```sql -ALTER TABLE table_name ADD INDEX index_name (column_list); -``` - -ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。 - -其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。 - -索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。 - -第三种方式:使用CREATE INDEX命令创建 - -```sql -CREATE INDEX index_name ON table_name (column_list); -``` - -CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引) - -删除索引 - -根据索引名删除普通索引、唯一索引、全文索引:`alter table 表名 drop KEY 索引名` - -```sql -alter table user_index drop KEY name; -alter table user_index drop KEY id_card; -alter table user_index drop KEY information; - -``` - - 删除主键索引:`alter table 表名 drop primary key`(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引): - - ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8yLzE5LzE2OTA0NTk2YjIxZTIwOWM?x-oss-process=image/format,png) - - 需要取消自增长再行删除: - -```sql -alter table user_index --- 重新定义字段 -MODIFY id int, -drop PRIMARY KEY - -``` - - 但通常不会删除主键,因为设计主键一定与业务逻辑无关。 - -## Redis篇 - -### 1、Redis持久化机制 - -Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。 -实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。 - -RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。) -AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。 -当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。 - - - -### 2、缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题 - -**一、缓存雪崩** - -我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 -(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。 -**解决办法:** -大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。 - -**二、缓存穿透** -缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。 -**解决办法;** -最常见的则是采用**布隆过滤器**,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 -另外也有一个更为**简单粗暴的方法**,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。 -5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢? - -对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。 -Bitmap: 典型的就是哈希表 -缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。 - -**布隆过滤器(推荐)** -就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。 -它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 -Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。 -Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。 -Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。 - -**三、缓存预热** -缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! -解决思路: -1、直接写个缓存刷新页面,上线时手工操作下; -2、数据量不大,可以在项目启动的时候自动进行加载; -3、定时刷新缓存; - -**四、缓存更新** -除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: -(1)定时去清理过期的缓存; -(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 -两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。 - -**五、缓存降级** -当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 -降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 -以参考日志级别设置预案: -(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; -(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; -(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; -(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。 - -服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。 - - - - -### 3、热点数据和冷数据是什么 - -**热点数据,缓存才有价值** -对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存 -对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。 -对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。 -**数据更新前至少读取两次,**缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。 -那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。 - -### 4、Memcache与Redis的区别都有哪些? - -1)、存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,redis可以持久化其数据 -2)、数据支持类型 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 ,提供list,set,zset,hash等数据结构的存储 -3)、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。 -4). value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。 -5)redis的速度比memcached快很多 -6)Redis支持数据的备份,即master-slave模式的数据备份。 - - - -### 5、单线程的redis为什么这么快 - - (一)纯内存操作 -(二)单线程操作,避免了频繁的上下文切换 -(三)采用了非阻塞I/O多路复用机制 - - - -### 6、redis的数据类型,以及每种数据类型的使用场景 - -回答:一共五种 -(一)String -这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。 -(二)hash -这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。 -(三)list -使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。 -(四)set -因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。 -另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。 -(五)sorted set -sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。 - - - -### 7、redis的过期策略以及内存淘汰机制 - -redis采用的是**定期删除+惰性删除策略。** -为什么不用定时删除策略? -定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略. -**定期删除+惰性删除是如何工作的呢?** -定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 -于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。 -采用定期删除+惰性删除就没其他问题了么? -不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。 -在redis.conf中有一行配置 - -``` -maxmemory-policy volatile-lru -``` - - -该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己) -**volatile-lru**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 -**volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 -**volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 -**allkeys-lru**:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 -**allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰 -**no-enviction**(驱逐):禁止驱逐数据,新写入操作会报错 -ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。 - -### 8、Redis 为什么是单线程的 - -官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问 -1)绝大部分请求是纯粹的内存操作(非常快速)2)采用单线程,避免了不必要的上下文切换和竞争条件 -3)非阻塞IO优点: - -- 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) - -- 支持丰富数据类型,支持string,list,set,sorted set,hash -- 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行 -- 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题 - -同时有多个子系统去set一个key。这个时候要注意什么呢? 不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。 -(1)如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可 -(2)如果对这个key操作,要求顺序: 分布式锁+时间戳。 假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。 -(3) 利用队列,将set方法变成串行访问也可以redis遇到高并发,如果保证读写key的一致性 -对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了。 - -### 9、Redis 常见性能问题和解决方案? - -(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件 -(2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次 -(3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内 -(4) 尽量避免在压力很大的主库上增加从库 -(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3… - -### 10、为什么Redis的操作是原子性的,怎么保证原子性的? - -对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。 -Redis的操作之所以是原子性的,是因为Redis是单线程的。 -Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。 -多个命令在并发中也是原子性的吗? -不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现. - -### 11、Redis事务 - -Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 -Redis会将一个事务中的所有命令序列化,然后按顺序执行。 -1.redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。 -2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行; -3.如果在一个事务中出现运行错误,那么正确的命令会被执行。 - -1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 -2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。 -3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。 -4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。 - - - -## SpringCloud篇 - -### 1、什么是SpringCloud - - Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 - -### 2、什么是微服务 - -微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。 - - - -### 3、SpringCloud有什么优势 - -使用 Spring Boot 开发分布式微服务时,我们面临以下问题 - -(1)与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。 - -(2)服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。 - -(3)冗余-分布式系统中的冗余问题。 - -(4)负载平衡 --负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。 - -(5)性能-问题 由于各种运营开销导致的性能问题。 - -(6)部署复杂性-Devops 技能的要求。 - - - -### 4、 **什么是服务熔断?什么是服务降级?** - -熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。 - -服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。 - - **Hystrix相关注解** - @EnableHystrix:开启熔断 - @HystrixCommand(fallbackMethod=”XXX”):声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。 - - ![a3e7daec2343b9188bfd745b7dfe0a93693.jpg](https://oscimg.oschina.net/oscnet/a3e7daec2343b9188bfd745b7dfe0a93693.jpg) - - - -### 5、 **Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?** - -Zookeeper保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用) - 1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。 - - 2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况: - ①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。 - ②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用) - ③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。 - - 因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪 - - - -### 6、SpringBoot和SpringCloud的区别? - -SpringBoot专注于快速方便的开发单个个体微服务。 - -SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来, - -为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务 - -SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系. - -SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。 - - - -### 7、负载平衡的意义什么? - - 在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源 -的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。 - - - -### 8、什么是Hystrix?它如何实现容错? - -Hystrix是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。 - -通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。 - -思考以下微服务 - - ![img](https://upload-images.jianshu.io/upload_images/13449584-395cfbeb00163ce1.png?imageMogr2/auto-orient/strip|imageView2/2/w/550/format/webp) - -假设如果上图中的微服务9失败了,那么使用传统方法我们将传播一个异常。但这仍然会导致整个系统崩溃。 - -随着微服务数量的增加,这个问题变得更加复杂。微服务的数量可以高达1000.这是hystrix出现的地方 我们将使用Hystrix在这种情况下的Fallback方法功能。我们有两个服务employee-consumer使用由employee-consumer公开的服务。 - -简化图如下所示 - - ![img](https://upload-images.jianshu.io/upload_images/13449584-60c5493aac0e15f2.png?imageMogr2/auto-orient/strip|imageView2/2/w/550/format/webp) - -现在假设由于某种原因,employee-producer公开的服务会抛出异常。我们在这种情况下使用Hystrix定义了一个回退方法。这种后备方法应该具有与公开服务相同的返回类型。如果暴露服务中出现异常,则回退方法将返回一些值。 - - - -### 9、什么是Hystrix断路器?我们需要它吗? - - 由于某些原因,employee-consumer公开服务会引发异常。在这种情况下使用Hystrix我们定义了一个回退方法。如果在公开服务中发生异常,则回退方法返回一些默认值。 - - ![img](https://upload-images.jianshu.io/upload_images/13449584-de1285a695fb6e50.png?imageMogr2/auto-orient/strip|imageView2/2/w/550/format/webp) - -如果firstPage method() 中的异常继续发生,则Hystrix电路将中断,并且员工使用者将一起跳过firtsPage方法,并直接调用回退方法。 断路器的目的是给第一页方法或第一页方法可能调用的其他方法留出时间,并导致异常恢复。可能发生的情况是,在负载较小的情况下,导致异常的问题有更好的恢复机会 。 - - ![img](https://upload-images.jianshu.io/upload_images/13449584-b52c979e802d20c1.png?imageMogr2/auto-orient/strip|imageView2/2/w/550/format/webp) - -### 10、说说 RPC 的实现原理 - -首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编 -解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列 -化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服 -务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果 -返回。 - - - -## Nginx篇 - -### 1、简述一下什么是Nginx,它有什么优势和功能? - -Nginx是一个web服务器和方向代理服务器,用于HTTP、HTTPS、SMTP、POP3和IMAP协议。因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。 - -> Nginx---Ngine X,是一款免费的、自由的、开源的、高性能HTTP服务器和反向代理服务器;也是一个IMAP、POP3、SMTP代理服务器;Nginx以其高性能、稳定性、丰富的功能、简单的配置和低资源消耗而闻名。 -> -> 也就是说Nginx本身就可以托管网站(类似于Tomcat一样),进行Http服务处理,也可以作为反向代理服务器 、负载均衡器和HTTP缓存。 -> -> Nginx 解决了服务器的C10K(就是在一秒之内连接客户端的数目为10k即1万)问题。它的设计不像传统的服务器那样使用线程处理请求,而是一个更加高级的机制—事件驱动机制,是一种异步事件驱动结构。 - -优点: -**(1)更快** -这表现在两个方面:一方面,在正常情况下,单次请求会得到更快的响应;另一方面,在高峰期(如有数以万计的并发请求),Nginx可以比其他Web服务器更快地响应请求。 -**(2)高扩展性,跨平台** -Nginx的设计极具扩展性,它完全是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。因此,当对某一个模块修复Bug或进行升级时,可以专注于模块自身,无须在意其他。而且在HTTP模块中,还设计了HTTP过滤器模块:一个正常的HTTP模块在处理完请求后,会有一串HTTP过滤器模块对请求的结果进行再处理。这样,当我们开发一个新的HTTP模块时,不但可以使用诸如HTTP核心模块、events模块、log模块等不同层次或者不同类型的模块,还可以原封不动地复用大量已有的HTTP过滤器模块。这种低耦合度的优秀设计,造就了Nginx庞大的第三方模块,当然,公开的第三方模块也如官方发布的模块一样容易使用。 -Nginx的模块都是嵌入到二进制文件中执行的,无论官方发布的模块还是第三方模块都是如此。这使得第三方模块一样具备极其优秀的性能,充分利用Nginx的高并发特性,因此,许多高流量的网站都倾向于开发符合自己业务特性的定制模块。 -**(3)高可靠性:用于反向代理,宕机的概率微乎其微** -高可靠性是我们选择Nginx的最基本条件,因为Nginx的可靠性是大家有目共睹的,很多家高流量网站都在核心服务器上大规模使用Nginx。Nginx的高可靠性来自于其核心框架代码的优秀设计、模块设计的简单性;另外,官方提供的常用模块都非常稳定,每个worker进程相对独立,master进程在1个worker进程出错时可以快速“拉起”新的worker子进程提供服务。 - -**(4)低内存消耗** -一般情况下,10 000个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB的内存,这是Nginx支持高并发连接的基础。 -**(5)单机支持10万以上的并发连接** -这是一个非常重要的特性!随着互联网的迅猛发展和互联网用户数量的成倍增长,各大公司、网站都需要应付海量并发请求,一个能够在峰值期顶住10万以上并发请求的Server,无疑会得到大家的青睐。理论上,Nginx支持的并发连接上限取决于内存,10万远未封顶。当然,能够及时地处理更多的并发请求,是与业务特点紧密相关的。 -**(6)热部署** -master管理进程与worker工作进程的分离设计,使得Nginx能够提供热部署功能,即可以在7×24小时不间断服务的前提下,升级Nginx的可执行文件。当然,它也支持不停止服务就更新配置项、更换日志文件等功能。 -**(7)最自由的BSD许可协议** -这是Nginx可以快速发展的强大动力。BSD许可协议不只是允许用户免费使用Nginx,它还允许用户在自己的项目中直接使用或修改Nginx源码,然后发布。这吸引了无数开发者继续为Nginx贡献自己的智慧。 -以上7个特点当然不是Nginx的全部,拥有无数个官方功能模块、第三方功能模块使得Nginx能够满足绝大部分应用场景,这些功能模块间可以叠加以实现更加强大、复杂的功能,有些模块还支持Nginx与Perl、Lua等脚本语言集成工作,大大提高了开发效率。这些特点促使用户在寻找一个Web服务器时更多考虑Nginx。 -选择Nginx的核心理由还是它能在支持高并发请求的同时保持高效的服务 - -### 2、Nginx是如何处理一个HTTP请求的呢? - -Nginx 是一个高性能的 Web 服务器,能够同时处理大量的并发请求。它结合多进程机制和异步机制 ,异步机制使用的是异步非阻塞方式 ,接下来就给大家介绍一下 Nginx 的多线程机制和异步非阻塞机制 。 - -**1、多进程机制** - -服务器每当收到一个客户端时,就有 服务器主进程 ( master process )生成一个 子进程( worker process )出来和客户端建立连接进行交互,直到连接断开,该子进程就结束了。 - -使用进程的好处是各个进程之间相互独立,不需要加锁,减少了使用锁对性能造成影响,同时降低编程的复杂度,降低开发成本。其次,采用独立的进程,可以让进程互相之间不会影响 ,如果一个进程发生异常退出时,其它进程正常工作, master 进程则很快启动新的 worker 进程,确保服务不会中断,从而将风险降到最低。 - -缺点是操作系统生成一个子进程需要进行 内存复制等操作,在资源和时间上会产生一定的开销。当有大量请求时,会导致系统性能下降 。 - -**2、异步非阻塞机制** - -每个工作进程 使用 异步非阻塞方式 ,可以处理 多个客户端请求 。 - -当某个 工作进程 接收到客户端的请求以后,调用 IO 进行处理,如果不能立即得到结果,就去 处理其他请求 (即为 非阻塞 );而 客户端 在此期间也 无需等待响应 ,可以去处理其他事情(即为 异步 )。 - -当 IO 返回时,就会通知此 工作进程 ;该进程得到通知,暂时 挂起 当前处理的事务去 响应客户端请求 。 - -### 3、列举一些Nginx的特性 - -1. Nginx服务器的特性包括: -2. 反向代理/L7负载均衡器 -3. 嵌入式Perl解释器 -4. 动态二进制升级 -5. 可用于重新编写URL,具有非常好的PCRE支持 - - - -### 4、请列举Nginx和Apache 之间的不同点 - -![1583476168205](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1583476168205.png) - - - -### 5、在Nginx中,如何使用未定义的服务器名称来阻止处理请求? - - 只需将请求删除的服务器就可以定义为: - -``` -Server{ - listen 80; - server_name ""; - return 444; -} -``` - - 这里,服务器名被保留为一个空字符串,它将在没有“主机”头字段的情况下匹配请求,而一个特殊的Nginx的非标准代码444被返回,从而终止连接。 - -一般推荐 worker 进程数与CPU内核数一致,这样一来不存在大量的子进程生成和管理任务,避免了进程之间竞争CPU 资源和进程切换的开销。而且 Nginx 为了更好的利用 多核特性 ,提供了 CPU 亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来 Cache 的失效。 - -对于每个请求,有且只有一个工作进程 对其处理。首先,每个 worker 进程都是从 master进程 fork 过来。在 master 进程里面,先建立好需要 listen 的 socket(listenfd) 之后,然后再 fork 出多个 worker 进程。 - -所有 worker 进程的 listenfd 会在新连接到来时变得可读 ,为保证只有一个进程处理该连接,所有 worker 进程在注册 listenfd 读事件前抢占 accept_mutex ,抢到互斥锁的那个进程注册 listenfd 读事件 ,在读事件里调用 accept 接受该连接。 - -当一个 worker 进程在 accept 这个连接之后,就开始读取请求、解析请求、处理请求,产生数据后,再返回给客户端 ,最后才断开连接。这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由 worker 进程来处理,而且只在一个 worker 进程中处理。 - -![640?](https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_jpg/UtWdDgynLdYJzfyu1L8rI8asia8NkuKt5X9ZBNgaawiaFg1KEAwZaibXuichhPNgHTZRqXLicd2avQzMMxOTc2mBDXA/640?) - -在 Nginx 服务器的运行过程中, 主进程和工作进程 需要进程交互。交互依赖于 Socket 实现的管道来实现。 - -### 6、请解释Nginx服务器上的Master和Worker进程分别是什么? - -- 主程序 Master process 启动后,通过一个 for 循环来 接收 和 处理外部信号 ; -- 主进程通过 fork() 函数产生 worker 子进程 ,每个子进程执行一个 for循环来实现Nginx服务器对事件的接收和处理 。 - - ![640?](https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/UtWdDgynLdYJzfyu1L8rI8asia8NkuKt5RiajKOy5hzmpA2NxqZBqakbyVj7QtricyBia1MEgtMOkzoiaF7Z1sFc1hg/640?) - - - -### 7、请解释代理中的正向代理和反向代理 - - 首先,代理服务器一般指局域网内部的机器通过代理服务器发送请求到互联网上的服务器,代理服务器一般作用在客户端。例如:GoAgent翻墙软件。我们的客户端在进行翻墙操作的时候,我们使用的正是正向代理,通过正向代理的方式,在我们的客户端运行一个软件,将我们的HTTP请求转发到其他不同的服务器端,实现请求的分发。 - - ![640?](https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/UtWdDgynLdYJzfyu1L8rI8asia8NkuKt5vXdia1RicjhSpzVkvmOuHDVRWMcljQh60Z90cmLHFQ4Md8cJq4Kn0thg/640?) - - 反向代理服务器作用在服务器端,它在服务器端接收客户端的请求,然后将请求分发给具体的服务器进行处理,然后再将服务器的相应结果反馈给客户端。Nginx就是一个反向代理服务器软件。 - - ![640?](https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/UtWdDgynLdYJzfyu1L8rI8asia8NkuKt5EGFQBBLqOG2wMNh27CPcndwibeMrRaQsU9q553ZkwsQaB7SNtr8aibJw/640?) - - 从上图可以看出:客户端必须设置正向代理服务器,当然前提是要知道正向代理服务器的IP地址,还有代理程序的端口。 -反向代理正好与正向代理相反,对于客户端而言代理服务器就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端。 - - ![640?](https://ss.csdn.net/p?https://mmbiz.qpic.cn/mmbiz_png/UtWdDgynLdYJzfyu1L8rI8asia8NkuKt5UOLvTTtAFI04Dbicmz7Cb23rjVwTia9bJicPYkhB7lSKUAO7YOzmXwE8A/640?) - - - -### 8、解释Nginx用途 - - Nginx服务器的最佳用法是在网络上部署动态HTTP内容,使用SCGI、WSGI应用程序服务器、用于脚本的FastCGI处理程序。它还可以作为负载均衡器。 - - - -## zookeeper篇 - -### 1\. ZooKeeper 是什么? - -ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 - -分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。 - -Zookeeper 保证了如下分布式一致性特性: - -(1)顺序一致性 - -(2)原子性 - -(3)单一视图 - -(4)可靠性 - -(5)实时性(最终一致性) - -客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求,这些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。 - -有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper 最新的 zxid。 - -### 2\. ZooKeeper 提供了什么? - -(1)文件系统 - -(2)通知机制 - -### 3.Zookeeper 文件系统 - -Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。 - -Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。 - -### 4\. ZAB 协议? - -ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。 - -ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。 - -当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步,当集群中超过半数机器与该 Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。 - -### 5\. 四种类型的数据节点 Znode - -(1)PERSISTENT-持久节点 - -除非手动删除,否则节点一直存在于 Zookeeper 上 - -(2)EPHEMERAL-临时节点 - -临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。 - -(3)PERSISTENT_SEQUENTIAL-持久顺序节点 - -基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。 - -(4)EPHEMERAL_SEQUENTIAL-临时顺序节点 - -基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。 - -### 6\. Zookeeper Watcher 机制 -- 数据变更通知 - -Zookeeper 允许客户端向服务端的某个 Znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。 - -工作机制: - -(1)客户端注册 watcher - -(2)服务端处理 watcher - -(3)客户端回调 watcher - -Watcher 特性总结: - -(1)一次性 - -无论是服务端还是客户端,一旦一个 Watcher 被 触 发 ,Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。 - -(2)客户端串行执行 - -客户端 Watcher 回调的过程是一个串行同步的过程。 - -(3)轻量 - -3.1、Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。 - -3.2、客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端,仅仅是在客户端请求中使用 boolean 类型属性进行了标记。 - -(4)watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket 进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。 - -(5)注册 watcher getData、exists、getChildren - -(6)触发 watcher create、delete、setData - -(7)当一个客户端连接到一个新的服务器上时,watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch 的。而当 client 重新连接时,如果需要的话,所有先前注册过的 watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未创建的 znode的 exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch 事件可能会被丢失。 - -### 7\. 客户端注册 Watcher 实现 - -(1)调用 getData()/getChildren()/exist()三个 API,传入 Watcher 对象 - -(2)标记请求 request,封装 Watcher 到 WatchRegistration - -(3)封装成 Packet 对象,发服务端发送 request - -(4)收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管理 - -(5)请求返回,完成注册。 - -### 8\. 服务端处理 Watcher 实现 - -(1)服务端接收 Watcher 并存储 - -接收到客户端请求,处理请求判断是否需要注册 Watcher,需要的话将数据节点的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了 Watcher 的 process 接口,此时可以看成一个 Watcher 对象)存储在WatcherManager 的 WatchTable 和 watch2Paths 中去。 - -(2)Watcher 触发 - -以服务端接收到 setData() 事务请求触发 NodeDataChanged 事件为例: - -2.1 封装 WatchedEvent - -将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个 WatchedEvent 对象 - -2.2 查询 Watcher - -从 WatchTable 中根据节点路径查找 Watcher - -2.3 没找到;说明没有客户端在该数据节点上注册过 Watcher - -2.4 找到;提取并从 WatchTable 和 Watch2Paths 中删除对应 Watcher(从这里可以看出 Watcher 在服务端是一次性的,触发一次就失效了) - -(3)调用 process 方法来触发 Watcher - -这里 process 主要就是通过 ServerCnxn 对应的 TCP 连接发送 Watcher 事件通知。 - -### 9\. 客户端回调 Watcher - -客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher。 - -客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了。 - -### 10\. ACL 权限控制机制 - -UGO(User/Group/Others) - -目前在 Linux/Unix 文件系统中使用,也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。 - -ACL(Access Control List)访问控制列表 - -包括三个方面: - -权限模式(Scheme) - -(1)IP:从 IP 地址粒度进行权限控制 - -(2)Digest:最常用,用类似于 username:password 的权限标识来进行权限配置,便于区分不同应用来进行权限控制 - -(3)World:最开放的权限控制方式,是一种特殊的 digest 模式,只有一个权限标识“world:anyone” - -(4)Super:超级用户 - -授权对象 - -授权对象指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器灯。 - -权限 Permission - -(1)CREATE:数据节点创建权限,允许授权对象在该 Znode 下创建子节点 - -(2)DELETE:子节点删除权限,允许授权对象删除该数据节点的子节点 - -(3)READ:数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等 - -(4)WRITE:数据节点更新权限,允许授权对象对该数据节点进行更新操作 - -(5)ADMIN:数据节点管理权限,允许授权对象对该数据节点进行 ACL 相关设置操作 - -### 11\. Chroot 特性 - -3.2.0 版本后,添加了 Chroot 特性,该特性允许每个客户端为自己设置一个命名空间。如果一个客户端设置了 Chroot,那么该客户端对服务器的任何操作,都将会被限制在其自己的命名空间下。 - -通过设置 Chroot,能够将一个客户端应用于 Zookeeper 服务端的一颗子树相对应,在那些多个应用公用一个 Zookeeper 进群的场景下,对实现不同应用间的相互隔离非常有帮助。 - -### 12\. 会话管理 - -分桶策略:将类似的会话放在同一区块中进行管理,以便于 Zookeeper 对会话进行不同区块的隔离处理以及同一区块的统一处理。 - -分配原则:每个会话的“下次超时时间点”(ExpirationTime) - -计算公式: - -ExpirationTime_ = currentTime + sessionTimeout - -ExpirationTime = (ExpirationTime_ / ExpirationInrerval + 1) * - -ExpirationInterval , ExpirationInterval 是指 Zookeeper 会话超时检查时间间隔,默认 tickTime - -### 13\. 服务器角色 - -Leader - -(1)事务请求的唯一调度和处理者,保证集群事务处理的顺序性 - -(2)集群内部各服务的调度者 - -Follower - -(1)处理客户端的非事务请求,转发事务请求给 Leader 服务器 - -(2)参与事务请求 Proposal 的投票 - -(3)参与 Leader 选举投票 - -Observer - -(1)3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力 - -(2)处理客户端的非事务请求,转发事务请求给 Leader 服务器 - -(3)不参与任何形式的投票 - -### 14\. Zookeeper 下 Server 工作状态 - -服务器具有四种状态,分别是 LOOKING、FOLLOWING、LEADING、OBSERVING。 - -(1)LOOKING:寻 找 Leader 状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。 - -(2)FOLLOWING:跟随者状态。表明当前服务器角色是 Follower。 - -(3)LEADING:领导者状态。表明当前服务器角色是 Leader。 - -(4)OBSERVING:观察者状态。表明当前服务器角色是 Observer。 - -### 15\. 数据同步 - -整个集群完成 Leader 选举之后,Learner(Follower 和 Observer 的统称)回向Leader 服务器进行注册。当 Learner 服务器想 Leader 服务器完成注册后,进入数据同步环节。 - -数据同步流程:(均以消息传递的方式进行) - -Learner 向 Learder 注册 - -数据同步 - -同步确认 - -Zookeeper 的数据同步通常分为四类: - -(1)直接差异化同步(DIFF 同步) - -(2)先回滚再差异化同步(TRUNC+DIFF 同步) - -(3)仅回滚同步(TRUNC 同步) - -(4)全量同步(SNAP 同步) - -在进行数据同步前,Leader 服务器会完成数据同步初始化: - -peerLastZxid: - -· 从 learner 服务器注册时发送的 ACKEPOCH 消息中提取 lastZxid(该Learner 服务器最后处理的 ZXID) - -minCommittedLog: - -· Leader 服务器 Proposal 缓存队列 committedLog 中最小 ZXIDmaxCommittedLog: - -· Leader 服务器 Proposal 缓存队列 committedLog 中最大 ZXID直接差异化同步(DIFF 同步) - -· 场景:peerLastZxid 介于 minCommittedLog 和 maxCommittedLog之间先回滚再差异化同步(TRUNC+DIFF 同步) - -· 场景:当新的 Leader 服务器发现某个 Learner 服务器包含了一条自己没有的事务记录,那么就需要让该 Learner 服务器进行事务回滚--回滚到 Leader服务器上存在的,同时也是最接近于 peerLastZxid 的 ZXID仅回滚同步(TRUNC 同步) - -· 场景:peerLastZxid 大于 maxCommittedLog - -全量同步(SNAP 同步) - -· 场景一:peerLastZxid 小于 minCommittedLog - -· 场景二:Leader 服务器上没有 Proposal 缓存队列且 peerLastZxid 不等于 lastProcessZxid - -### 16\. zookeeper 是如何保证事务的顺序一致性的? - -zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch( 时期; 纪元; 世; 新时代)用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。 - -### 17\. 分布式集群中为什么会有 Master? - -在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行leader 选举。 - -### 18\. zk 节点宕机如何处理? - -Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。 - -如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失; - -如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。 - -ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。 - -所以 - -3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5) - -2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票<=1) - -### 19\. zookeeper 负载均衡和 nginx 负载均衡区别 - -zk 的负载均衡是可以调控,nginx 只是能调权重,其他需要可控的都需要自己写插件;但是 nginx 的吞吐量比 zk 大很多,应该说按业务选择用哪种方式。 - -### 20\. Zookeeper 有哪几种几种部署模式? - -部署模式:单机模式、伪集群模式、集群模式。 - -### 21\. 集群最少要几台机器,集群规则是怎样的? - -集群规则为 2N+1 台,N>0,即 3 台。 - -### 22\. 集群支持动态添加机器吗? - -其实就是水平扩容了,Zookeeper 在这方面不太好。两种方式: - -全部重启:关闭所有 Zookeeper 服务,修改配置之后启动。不影响之前客户端的会话。 - -逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。 - -3.5 版本开始支持动态扩容。 - -### 23\. Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的? - -不是。官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。 - -为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。 - -一般是客户端执行 getData(“/节点 A”,true),如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。 - -在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。 - -### 24\. Zookeeper 的 java 客户端都有哪些? - -java 客户端:zk 自带的 zkclient 及 Apache 开源的 Curator。 - -### 25\. chubby 是什么,和 zookeeper 比你怎么看? - -chubby 是 google 的,完全实现 paxos 算法,不开源。zookeeper 是 chubby的开源实现,使用 zab 协议,paxos 算法的变种。 - -### 26\. 说几个 zookeeper 常用的命令。 - -常用命令:ls get set create delete 等。 - -### 27\. ZAB 和 Paxos 算法的联系与区别? - -相同点: - -(1)两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行 - -(2)Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交 - -(3)ZAB 协议中,每个 Proposal 中都包含一个 epoch 值来代表当前的 Leader周期,Paxos 中名字为 Ballot - -不同点: - -ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。 - -![](https://user-gold-cdn.xitu.io/2020/1/13/16f9f49d3f005e23?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -### 28\. Zookeeper 的典型应用场景 - -Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布和订阅。 - -通过对 Zookeeper 中丰富的数据节点进行交叉使用,配合 Watcher 事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能,如: - -(1)数据发布/订阅 - -(2)负载均衡 - -(3)命名服务 - -(4)分布式协调/通知 - -(5)集群管理 - -(6)Master 选举 - -(7)分布式锁 - -(8)分布式队列 - -**数据发布/订阅** - -介绍 - -数据发布/订阅系统,即所谓的配置中心,顾名思义就是发布者发布数据供订阅者进行数据订阅。 - -目的 - -动态获取数据(配置信息) - -实现数据(配置信息)的集中式管理和数据的动态更新 - -设计模式 - -Push 模式 - -Pull 模式 - -数据(配置信息)特性 - -(1)数据量通常比较小 - -(2)数据内容在运行时会发生动态更新 - -(3)集群中各机器共享,配置一致 - -如:机器列表信息、运行时开关配置、数据库配置信息等 - -基于 Zookeeper 的实现方式 - -· 数据存储:将数据(配置信息)存储到 Zookeeper 上的一个数据节点 - -· 数据获取:应用在启动初始化节点从 Zookeeper 数据节点读取数据,并在该节点上注册一个数据变更 Watcher - -· 数据变更:当变更数据时,更新 Zookeeper 对应节点数据,Zookeeper会将数据变更通知发到各客户端,客户端接到通知后重新读取变更后的数据即可。 - -**负载均衡** - -zk 的命名服务 - -命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。 - -**分布式通知和协调** - -对于系统调度来说:操作人员发送通知实际是通过控制台改变某个节点的状态,然后 zk 将这些变化发送给注册了这个节点的 watcher 的所有客户端。 - -对于执行情况汇报:每个工作进程都在某个目录下创建一个临时节点。并携带工作的进度数据,这样汇总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。 - -**zk 的命名服务(文件系统)** - -命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,即是唯一的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。 - -**zk 的配置管理(文件系统、通知机制)** - -程序分布式的部署在不同的机器上,将程序的配置信息放在 zk 的 znode 下,当有配置发生改变时,也就是 znode 发生变化时,可以通过改变 zk 中某个目录节点的内容,利用 watcher 通知给各个客户端,从而更改配置。 - -**Zookeeper 集群管理(文件系统、通知机制)** - -所谓集群管理无在乎两点:是否有机器退出和加入、选举 master。 - -对于第一点,所有机器约定在父目录下创建临时目录节点,然后监听父目录节点 - -的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper 的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。 - -新机器加入也是类似,所有机器收到通知:新兄弟目录加入,highcount 又有了,对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为 master 就好。 - -**Zookeeper 分布式锁(文件系统、通知机制)** - -有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。 - -对于第一类,我们将 zookeeper 上的一个 znode 看作是一把锁,通过 createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的 distribute_lock 节点就释放出锁。 - -对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选 master 一样,编号最小的获得锁,用完删除,依次方便。 - -Zookeeper 队列管理(文件系统、通知机制) - -两种类型的队列: - -(1)同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。 - -(2)队列按照 FIFO 方式进行入队和出队操作。 - -第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。 - -第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建 PERSISTENT_SEQUENTIAL 节点,创建成功时Watcher 通知等待的队列,队列删除序列号最小的节点用以消费。此场景下Zookeeper 的 znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。 - - - -## kafka篇 - - - -### 1、如何获取 topic 主题的列表 - -bin/kafka-topics.sh --list --zookeeper localhost:2181 - -### 2、生产者和消费者的命令行是什么? - -**生产者在主题上发布消息:** - -bin/kafka-console-producer.sh --broker-list 192.168.43.49:9092 --topicHello-Kafka - -注意这里的 IP 是 server.properties 中的 listeners 的配置。接下来每个新行就是输入一条新消息。 - -**消费者接受消息:** - -bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topicHello-Kafka --from-beginning - -### 3、consumer 是推还是拉? - -Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消息推送到 consumer,也就是 pull 还 push。在这方面,Kafka 遵循了一种大部分消息系统共同的传统的设计:producer 将消息推送到 broker,consumer 从broker 拉取消息。 - -一些消息系统比如 Scribe 和 Apache Flume 采用了 push 模式,将消息推送到下游的 consumer。这样做有好处也有坏处:由 broker 决定消息推送的速率,对于不同消费速率的 consumer 就不太好处理了。消息系统都致力于让 consumer 以最大的速率最快速的消费消息,但不幸的是,push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时,consumer 恐怕就要崩溃了。最终 Kafka 还是选取了传统的 pull 模式。 - -Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据 。Push 模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull 模式下,consumer 就可以根据自己的消费能力去决定这些策略。 - -Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询,直到新消息到 t 达。为了避免这点,Kafka 有个参数可以让 consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发送)。 - -### 4、讲讲 kafka 维护消费状态跟踪的方法 - -大部分消息系统在 broker 端的维护消息被消费的记录:一个消息被分发到consumer 后 broker 就马上进行标记或者等待 customer 的通知后进行标记。这样也可以在消息在消费后立马就删除以减少空间占用。 - -但是这样会不会有什么问题呢?如果一条消息发送出去之后就立即被标记为消费过的,旦 consumer 处理消息时失败了(比如程序崩溃)消息就丢失了。为了解决这个问题,很多消息系统提供了另外一个个功能:当消息被发送出去之后仅仅被标记为已发送状态,当接到 consumer 已经消费成功的通知后才标记为已被消费的状态。这虽然解决了消息丢失的问题,但产生了新问题,首先如果 consumer处理消息成功了但是向 broker 发送响应时失败了,这条消息将被消费两次。第二个问题时,broker 必须维护每条消息的状态,并且每次都要先锁住消息然后更改状态然后释放锁。这样麻烦又来了,且不说要维护大量的状态数据,比如如果消息发送出去但没有收到消费成功的通知,这条消息将一直处于被锁定的状态,Kafka 采用了不同的策略。Topic 被分成了若干分区,每个分区在同一时间只被一个 consumer 消费。这意味着每个分区被消费的消息在日志中的位置仅仅是一个简单的整数:offset。这样就很容易标记每个分区消费状态就很容易了,仅仅需要一个整数而已。这样消费状态的跟踪就很简单了。 - -这带来了另外一个好处:consumer 可以把 offset 调成一个较老的值,去重新消费老的消息。这对传统的消息系统来说看起来有些不可思议,但确实是非常有用的,谁规定了一条消息只能被消费一次呢? - - -### 5、讲一下主从同步 - -Kafka允许topic的分区拥有若干副本,这个数量是可以配置的,你可以为每个topci配置副本的数量。Kafka会自动在每个个副本上备份数据,所以当一个节点down掉时数据依然是可用的。 - -Kafka的副本功能不是必须的,你可以配置只有一个副本,这样其实就相当于只有一份数据。 - -### 6、为什么需要消息系统,mysql 不能满足需求吗? - -**(1)解耦:** - -允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。 - -**(2)冗余:** - -消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 - -**(3)扩展性:** - -因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。 - -**(4)灵活性 & 峰值处理能力:** - -在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。 - -**(5)可恢复性:** - -系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 - -**(6)顺序保证:** - -在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性) - -**(7)缓冲:** - -有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。 - -**(8)异步通信:** - -很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。 - -### 7、Zookeeper 对于 Kafka 的作用是什么? - -Zookeeper 是一个开放源码的、高性能的协调服务,它用于 Kafka 的分布式应用。 - -Zookeeper 主要用于在集群中不同节点之间进行通信 - -在 Kafka 中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如: leader 检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。 - -8、数据传输的事务定义有哪三种? - -和 MQTT 的事务定义一样都是 3 种。 - -(1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输 - -(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输. - -(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被一次而且仅仅被传输一次,这是大家所期望的 - -### 9、Kafka 判断一个节点是否还活着有那两个条件? - -(1)节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接 - -(2)如果节点是个 follower,他必须能及时的同步 leader 的写操作,延时不能太久 - -### 10、Kafka 与传统 MQ 消息系统之间有三个关键区别 - -(1).Kafka 持久化日志,这些日志可以被重复读取和无限期保留 - -(2).Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性 - -(3).Kafka 支持实时的流式处理 - -### 11、讲一讲 kafka 的 ack 的三种机制 - -request.required.acks 有三个值 0 1 -1(all) - -0:生产者不会等待 broker 的 ack,这个延迟最低但是存储的保证最弱当 server 挂掉的时候就会丢数据。 - -1:服务端会等待 ack 值 leader 副本确认接收到消息后发送 ack 但是如果 leader挂掉后他不确保是否复制完成新 leader 也会导致数据丢失。 - --1(all):服务端会等所有的 follower 的副本受到数据后才会受到 leader 发出的ack,这样数据不会丢失 - -### 12、消费者如何不自动提交偏移量,由应用提交? - -将 auto.commit.offset 设为 false,然后在处理一批消息后 commitSync() 或者异步提交 commitAsync() - -即: - -```java -ConsumerRecords<> records = consumer.poll(); -for (ConsumerRecord<> record : records){ - 。。。 - tyr{ - consumer.commitSync() - } - 。。。 - -``` - -### 13、消费者故障,出现活锁问题如何解决? - -出现“活锁”的情况,是它持续的发送心跳,但是没有处理。为了预防消费者在这种情况下一直持有分区,我们使用 max.poll.interval.ms 活跃检测机制。 在此基础上,如果你调用的 poll 的频率大于最大间隔,则客户端将主动地离开组,以便其他消费者接管该分区。 发生这种情况时,你会看到 offset 提交失败(调用commitSync()引发的 CommitFailedException)。这是一种安全机制,保障只有活动成员能够提交 offset。所以要留在组中,你必须持续调用 poll。 - -消费者提供两个配置设置来控制 poll 循环: - -max.poll.interval.ms:增大 poll 的间隔,可以为消费者提供更多的时间去处理返回的消息(调用 poll(long)返回的消息,通常返回的消息都是一批)。缺点是此值越大将会延迟组重新平衡。 - -max.poll.records:此设置限制每次调用 poll 返回的消息数,这样可以更容易的预测每次 poll 间隔要处理的最大值。通过调整此值,可以减少 poll 间隔,减少重新平衡分组的 - -对于消息处理时间不可预测地的情况,这些选项是不够的。 处理这种情况的推荐方法是将消息处理移到另一个线程中,让消费者继续调用 poll。 但是必须注意确保已提交的 offset 不超过实际位置。另外,你必须禁用自动提交,并只有在线程完成处理后才为记录手动提交偏移量(取决于你)。 还要注意,你需要 pause 暂停分区,不会从 poll 接收到新消息,让线程处理完之前返回的消息(如果你的处理能力比拉取消息的慢,那创建新线程将导致你机器内存溢出)。 - -![](data:image/svg+xml;utf8,) - -### 14、如何控制消费的位置 - -kafka 使用 seek(TopicPartition, long)指定新的消费位置。用于查找服务器保留的最早和最新的 offset 的特殊的方法也可用(seekToBeginning(Collection) 和seekToEnd(Collection)) - -### 15、kafka 分布式(不是单机)的情况下,如何保证消息的顺序消费? - -Kafka 分布式的单位是 partition,同一个 partition 用一个 write ahead log 组织,所以可以保证 FIFO 的顺序。不同 partition 之间不能保证顺序。但是绝大多数用户都可以通过 message key 来定义,因为同一个 key 的 message 可以保证只发送到同一个 partition。 - -Kafka 中发送 1 条消息的时候,可以指定(topic, partition, key) 3 个参数。partiton 和 key 是可选的。如果你指定了 partition,那就是所有消息发往同 1个 partition,就是有序的。并且在消费端,Kafka 保证,1 个 partition 只能被1 个 consumer 消费。或者你指定 key( 比如 order id),具有同 1 个 key 的所有消息,会发往同 1 个 partition。 - -### 16、kafka 的高可用机制是什么? - -这个问题比较系统,回答出 kafka 的系统特点,leader 和 follower 的关系,消息读写的顺序即可。 - -### 17、kafka 如何减少数据丢失 - -Kafka到底会不会丢数据(data loss)? 通常不会,但有些情况下的确有可能会发生。下面的参数配置及Best practice列表可以较好地保证数据的持久性(当然是trade-off,牺牲了吞吐量)。 - -* block.on.buffer.full = true - -* acks = all - -* retries = MAX_VALUE - -* max.in.flight.requests.per.connection = 1 - -* 使用KafkaProducer.send(record, callback) - -* callback逻辑中显式关闭producer:close(0) - -* unclean.leader.election.enable=false - -* replication.factor = 3 - -* min.insync.replicas = 2 - -* replication.factor > min.insync.replicas - -* enable.auto.commit=false - -* 消息处理完成之后再提交位移 - -### 18、kafka 如何不消费重复数据?比如扣款,我们不能重复的扣。 - -其实还是得结合业务来思考,我这里给几个思路: - -比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。 - -比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。 - -比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。 - -比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。 - - - -## MQ篇 - - - -### 1、为什么使用MQ - - **核心:解耦,异步,削峰** - -**1)解耦:**A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃......A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 - -就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。 - -**(2)异步:**A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。 - -**(3)削峰:**减少高峰时期对服务器压力。 - - - -### 2、MQ优缺点 - -优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。 - -**缺点有以下几个:** - -**系统可用性降低** -系统引入的外部依赖越多,越容易挂掉。万一 MQ 挂了,MQ 一挂,整套系统崩溃,你不就完了? - -**系统复杂度提高** -硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?问题一大堆。 - -**一致性问题** -A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。 - -### 3、Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别? - -对于吞吐量来说kafka和RocketMQ支撑高吞吐,ActiveMQ和RabbitMQ比他们低一个数量级。对于延迟量来说RabbitMQ是最低的。 - -**1.从社区活跃度** - -按照目前网络上的资料,RabbitMQ 、activeM 、ZeroMQ 三者中,综合来看,RabbitMQ 是首选。 - -**2.持久化消息比较** - -ActiveMq 和RabbitMq 都支持。持久化消息主要是指我们机器在不可抗力因素等情况下挂掉了,消息不会丢失的机制。 - -**3.综合技术实现** - -可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统等等。 - -RabbitMq / Kafka 最好,ActiveMq 次之,ZeroMq 最差。当然ZeroMq 也可以做到,不过自己必须手动写代码实现,代码量不小。尤其是可靠性中的:持久性、投递确认、发布者证实和高可用性。 - -**4.高并发** - -毋庸置疑,RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言。 - -**5.比较关注的比较, RabbitMQ 和 Kafka** - -RabbitMq 比Kafka 成熟,在可用性上,稳定性上,可靠性上, RabbitMq 胜于 Kafka (理论上)。 - -另外,Kafka 的定位主要在日志等方面, 因为Kafka 设计的初衷就是处理日志的,可以看做是一个日志(消息)系统一个重要组件,针对性很强,所以 如果业务方面还是建议选择 RabbitMq 。 - -还有就是,Kafka 的性能(吞吐量、TPS )比RabbitMq 要高出来很多。 - - -![1583479010214](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1583479010214.png) - - - -### 4、如何保证高可用的? - -RabbitMQ 是比较有代表性的,因为是**基于主从**(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。 - -单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式 - -普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你**创建的 queue,只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。**这方案主要是提高吞吐量的**,就是说让集群中多个节点来服务某个 queue 的读写操作。 - -镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。 - -Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。这就是天然的分布式消息队列,就是说一个 topic 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。 - -### 5、如何保证消息的可靠传输?如果消息丢了怎么办 - -数据的丢失问题,可能出现在生产者、MQ、消费者中 - -生产者丢失:生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。吞吐量会下来,因为太耗性能。所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个ack消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收到了。所以一般在生产者这块避免数据丢失,都是用confirm机制的。 - -MQ中丢失:就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。设置持久化有两个步骤:创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是不会持久化 queue 里的数据。第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。 - -消费端丢失:你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。这个时候得用 RabbitMQ 提供的ack机制,简单来说,就是你关闭 RabbitMQ 的自动ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。 - ![img](https://img-blog.csdnimg.cn/20181224153229925.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xldHR5aXNtZQ==,size_16,color_FFFFFF,t_70) - - - -### 6、如何保证消息的顺序性 - -先看看顺序会错乱的场景:RabbitMQ:一个 queue,多个 consumer,这不明显乱了; - - ![img](https://img-blog.csdnimg.cn/20181224153636584.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xldHR5aXNtZQ==,size_16,color_FFFFFF,t_70) - - 解决: - - ![img](https://img-blog.csdnimg.cn/20181224153717396.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xldHR5aXNtZQ==,size_16,color_FFFFFF,t_70) - - - -### 7、 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? - -消息积压处理办法:临时紧急扩容: - -先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。 -新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 -然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 -接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 -等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。 -MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。 - -mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。 - - - -### 8、设计MQ的思路 - -比如说这个消息队列系统,我们从以下几个角度来考虑一下: - -首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了? - -其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。 - -其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。 - -能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。 - - - -## Elasticsearch篇 - -### 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 - -面试官:想了解应聘者之前公司接触的 ES 使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。 - -解答:如实结合自己的实践场景回答即可。 - -比如:ES 集群架构 13 个节点,索引根据通道不同共 20+索引,根据日期,每日递增 20+,索引:10 分片,每日递增 1 亿+数据,每个通道每天索引大小控制:150GB 之内。 - -仅索引层面调优手段: - -##### 1.1、设计阶段调优 - -(1)根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引; - -(2)使用别名进行索引管理; - -(3)每天凌晨定时对索引做 force_merge 操作,以释放空间; - -(4)采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink操作,以缩减存储; - -(5)采取 curator 进行索引的生命周期管理; - -(6)仅针对需要分词的字段,合理的设置分词器; - -(7)Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。…….. - -##### 1.2、写入调优 - -(1)写入前副本数设置为 0; - -(2)写入前关闭 refresh_interval 设置为-1,禁用刷新机制; - -(3)写入过程中:采取 bulk 批量写入; - -(4)写入后恢复副本数和刷新间隔; - -(5)尽量使用自动生成的 id。 - -##### 1.3、查询调优 - -(1)禁用 wildcard; - -(2)禁用批量 terms(成百上千的场景); - -(3)充分利用倒排索引机制,能 keyword 类型尽量 keyword; - -(4)数据量大时候,可以先基于时间敲定索引再检索; - -(5)设置合理的路由机制。 - -##### 1.4、其他调优 - -部署调优,业务调优等。 - -上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。 - -### 2、elasticsearch 的倒排索引是什么 - -面试官:想了解你对基础概念的认知。 - -解答:通俗解释一下就可以。 - -传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。 - -而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。 - -![img](https://user-gold-cdn.xitu.io/2019/12/25/16f3cd47d11e0d11?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -学术的解答方式: - -倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。 - -加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。 - -lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点: - -(1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间; - -(2)查询速度快。O(len(str))的查询时间复杂度。 - -### 3、elasticsearch 索引数据多了怎么办,如何调优,部署 - -面试官:想了解大数据量的运维能力。 - -解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。 - -如何调优,正如问题 1 所说,这里细化一下: - -##### 3.1 动态索引层面 - -基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog_index_时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的32 次幂-1,索引存储达到了 TB+甚至更大。 - -一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。 - -##### 3.2 存储层面 - -冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。 - -对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。 - -##### 3.3 部署层面 - -一旦之前没有规划,这里就属于应急策略。 - -结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。 - -### 4、elasticsearch 是如何实现 master 选举的 - -面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。 - -解答: - -前置前提: - -(1)只有候选主节点(master:true)的节点才能成为主节点。 - -(2)最小主节点数(min_master_nodes)的目的是防止脑裂。 - -核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。选举流程大致描述如下: - -第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 - -discovery.zen.minimum_master_nodes; - -第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回; - -若两节点都为候选主节点,则 id 小的值会主节点。注意这里的 id 为 string 类型。 - -题外话:获取节点 id 的方法。 - -``` -1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name - -2ip port heapPercent heapMax id name复制代码 -``` - - - -### 5、详细描述一下 Elasticsearch 索引文档的过程 - -面试官:想了解 ES 的底层原理,不再只关注业务层面了。 - -解答: - -这里的索引文档应该理解为文档写入 ES,创建索引的过程。 - -文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程。 - -记住官方文档中的这个图。 - - ![img](https://user-gold-cdn.xitu.io/2019/12/25/16f3cd47d2b0df73?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。) - -第二步:节点 1 接受到请求后,使用文档_id 来确定文档属于分片 0。请求会被转到另外的节点,假定节点 3。因此分片 0 的主分片分配到节点 3 上。 - -第三步:节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1和节点 2 的副本分片上,等待结果返回。所有的副本分片都报告成功,节点 3 将向协调节点(节点 1)报告成功,节点 1 向请求客户端报告写入成功。 - -如果面试官再问:第二步中的文档获取分片的过程? - -回答:借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。 - -``` -1shard = hash(_routing) % (num_of_primary_shards)复制代码 -``` - - - -### 6、详细描述一下 Elasticsearch 搜索的过程? - -面试官:想了解 ES 搜索的底层原理,不再只关注业务层面了。 - -解答: - -搜索拆解为“query then fetch” 两个阶段。 - -query 阶段的目的:定位到位置,但不取。 - -步骤拆解如下: - -(1)假设一个索引数据有 5 主+1 副本 共 10 分片,一次请求会命中(主或者副本分片中)的一个。 - -(2)每个分片在本地进行查询,结果返回到本地有序的优先队列中。 - -(3)第 2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。 - -fetch 阶段的目的:取数据。 - -路由节点获取所有文档,返回给客户端。 - - - -### 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 - -面试官:想了解对 ES 集群的运维能力。 - -解答: - -(1)关闭缓存 swap; - -(2)堆内存设置为:Min(节点内存/2, 32GB); - -(3)设置最大文件句柄数; - -(4)线程池+队列大小根据业务需要做调整; - -(5)磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。 - - - -### 8、lucence 内部结构是什么? - -面试官:想了解你的知识面的广度和深度。 - -解答: - -![img](data:image/svg+xml;utf8,) - -Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一些。 - - - -### 9、Elasticsearch 是如何实现 Master 选举的? - -(1)Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分; - -(2)对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。 - -(3)如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。 - -(4)补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http 功能*。 - - - -### 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个 - -选了一个 master,另外 10 个选了另一个 master,怎么办? - -(1)当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题; - -(3)当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data节点,避免脑裂问题。 - - - -### 11、客户端在和集群连接时,如何选择特定的节点执行请求的? - -TransportClient 利用 transport 模块远程连接一个 elasticsearch 集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的 transport 地址,并以 轮询 的方式与这些地址进行通信。 - - - -### 12、详细描述一下 Elasticsearch 索引文档的过程。 - -协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。 - -``` -shard = hash(document_id) % (num_of_primary_shards)复制代码 -``` - -(1)当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就叫做 refresh; - -(2)当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 ,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush; - -(3)在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。 - -(4)flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时; - -![img](https://user-gold-cdn.xitu.io/2019/12/25/16f3cd48799304d6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -补充:关于 Lucene 的 Segement: - -(1)Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。 - -(2)段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。 - -(3)对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。 - -(4)为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。 - - - -### 13、详细描述一下 Elasticsearch 更新和删除文档的过程。 - -(1)删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更; - -(2)磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del 文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。 - -(3)在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del 文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。 - -### 14、详细描述一下 Elasticsearch 搜索的过程。 - -(1)搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch; - -(2)在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。 - -PS:在搜索的时候是会查询 Filesystem Cache 的,但是有部分数据还在 MemoryBuffer,所以搜索是近实时的。 - -(3)每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。 - -(4)接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并 丰 富 文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。 +**9. 安全性** -(5)补充:Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。* +Java 通常被用在网络环境中,为此,Java 提供了一个安全机制以防止恶意代码的攻击。除了 Java 语言具有许多的安全特性以外,Java 还对通过网络下载的类增加一个安全防范机制,分配不同的名字空间以防替代本地的同名类,并包含安全管理机制。 -![](https://user-gold-cdn.xitu.io/2019/12/26/16f4126064267b88?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +Java 语言的众多特性使其在众多的编程语言中占有较大的市场份额,Java 语言对对象的支持和强大的 API 使得编程工作变得更加容易和快捷,大大降低了程序的开发成本。Java 的“一次编写,到处执行”正是它吸引众多商家和编程人员的一大优势。 -### 15、在 Elasticsearch 中,是怎么根据一个词找到对应的倒排索引的? +### 2. JDK和JRE和JVM的区别 +**1. JDK** -(1)Lucene的索引过程,就是按照全文检索的基本过程,将倒排表写成此文件格式的过程。 +JDK(Java SE Development Kit),**Java标准的开发包,提供了编译、运行Java程序所需要的各种工具和资源**,包括了Java编译器、Java运行时环境、以及常用的Java类库等。 -(2)Lucene的搜索过程,就是按照此文件格式将索引进去的信息读出来,然后计算每篇文档打分(score)的过程。 - -### 16、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法? - -(1)64 GB 内存的机器是非常理想的, 但是 32 GB 和 16 GB 机器也是很常见的。少于 8 GB 会适得其反。 - -(2)如果你要在更快的 CPUs 和更多的核心之间选择,选择更多的核心更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。 - -(3)如果你负担得起 SSD,它将远远超出任何旋转介质。 基于 SSD 的节点,查询和索引性能都有提升。如果你负担得起,SSD 是一个好的选择。 - -(4)即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离。 - -(5)请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。 在Elasticsearch 的几个地方,使用 Java 的本地序列化。 - -(6)通过设置 gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time 可以在集群重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。 - -(7)Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播。 - -(8)不要随意修改垃圾回收器(CMS)和各个线程池的大小。 - -(9)把你的内存的(少于)一半给 Lucene(但不要超过 32 GB!),通过ES_HEAP_SIZE 环境变量设置。 - -(10)内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可怕。 - -(11)Lucene 使用了大 量 的文件。同时,Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的值,如 64,000。 - -补充:索引阶段性能提升方法 - -(1)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。 - -(2)存储:使用 SSD - -(3)段和合并:Elasticsearch 默认值是 20 MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。另外还可以增加index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB,这可以在一次清空触发的时候在事务日志里积累出更大的段。 - -(4)如果你的搜索结果不需要近实时的准确度,考虑把每个索引的index.refresh_interval 改到 30s。 - -(5)如果你在做大批量导入,考虑通过设置 index.number_of_replicas: 0 关闭副本。 - -### 17、对于 GC 方面,在使用 Elasticsearch 时要注意什么? - -(1)倒排词典的索引需要常驻内存,无法 GC,需要监控 data node 上 segmentmemory 增长趋势。 - -(2)各类缓存,field cache, filter cache, indexing cache, bulk queue 等等,要设置合理的大小,并且要应该根据最坏的情况来看 heap 是否够用,也就是各类缓存全部占满的时候,还有 heap 空间可以分配给其他任务吗?避免采用 clear cache等“自欺欺人”的方式来释放内存。 - -(3)避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用scan & scroll api 来实现。 - -(4)cluster stats 驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过 tribe node 连接。 - -(5)想知道 heap 够不够,必须结合实际应用场景,并对集群的 heap 使用情况做持续的监控。 - -(6)根据监控数据理解内存需求,合理配置各类circuit breaker,将内存溢出风险降低到最低 - -### 18、Elasticsearch 对于大数据量(上亿量级)的聚合如何实现? - -Elasticsearch 提供的首个近似聚合是 cardinality 度量。它提供一个字段的基数,即该字段的 distinct 或者 unique 值的数目。它是基于 HLL 算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。 - -### 19、在并发情况下,Elasticsearch 如果保证读写一致? - -(1)可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突; - -(2)另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。 - -(3)对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,确保文档是最新版本。 - -### 20、如何监控 Elasticsearch 集群状态? - -Marvel 让你可以很简单的通过 Kibana 监控 Elasticsearch。你可以实时查看你的集群健康状态和性能,也可以分析过去的集群、索引和节点指标。 - -### 21、介绍下你们电商搜索的整体技术架构。 - -![](https://user-gold-cdn.xitu.io/2019/12/26/16f412606724edeb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -### 22、介绍一下你们的个性化搜索方案? - -基于word2vec和Elasticsearch实现个性化搜索 - -(1)基于word2vec、Elasticsearch和自定义的脚本插件,我们就实现了一个个性化的搜索服务,相对于原有的实现,新版的点击率和转化率都有大幅的提升; - -(2)基于word2vec的商品向量还有一个可用之处,就是可以用来实现相似商品的推荐; - -(3)使用word2vec来实现个性化搜索或个性化推荐是有一定局限性的,因为它只能处理用户点击历史这样的时序数据,而无法全面的去考虑用户偏好,这个还是有很大的改进和提升的空间; - -### 23、是否了解字典树? - -常用字典数据结构如下所示: - -![](https://user-gold-cdn.xitu.io/2019/12/26/16f41260671abdb0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -Trie 的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。它有 3 个基本性质: - -1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。 - -2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。 - -3)每个节点的所有子节点包含的字符都不相同。 - -![](https://user-gold-cdn.xitu.io/2019/12/26/16f4126066ec7ccf?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -(1)可以看到,trie 树每一层的节点数是 26^i 级别的。所以为了节省空间,我们还可以用动态链表,或者用数组来模拟动态。而空间的花费,不会超过单词数×单词长度。 - -(2)实现:对每个结点开一个字母集大小的数组,每个结点挂一个链表,使用左儿子右兄弟表示法记录这棵树; - -(3)对于中文的字典树,每个节点的子节点用一个哈希表存储,这样就不用浪费太大的空间,而且查询速度上可以保留哈希的复杂度 O(1)。 - -### 24、拼写纠错是如何实现的? - -(1)拼写纠错是基于编辑距离来实现;编辑距离是一种标准的方法,它用来表示经过插入、删除和替换操作从一个字符串转换到另外一个字符串的最小操作步数; - -(2)编辑距离的计算过程:比如要计算 batyu 和 beauty 的编辑距离,先创建一个7×8 的表(batyu 长度为 5,coffee 长度为 6,各加 2),接着,在如下位置填入黑色数字。其他格的计算过程是取以下三个值的最小值: - -如果最上方的字符等于最左方的字符,则为左上方的数字。否则为左上方的数字+1。(对于 3,3 来说为 0) - -左方数字+1(对于 3,3 格来说为 2) - -上方数字+1(对于 3,3 格来说为 2) - -最终取右下角的值即为编辑距离的值 3。 - -![](https://user-gold-cdn.xitu.io/2019/12/26/16f4126066d9e378?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -对于拼写纠错,我们考虑构造一个度量空间(Metric Space),该空间内任何关系满足以下三条基本条件: - -d(x,y) = 0 -- 假如 x 与 y 的距离为 0,则 x=y - -d(x,y) = d(y,x) -- x 到 y 的距离等同于 y 到 x 的距离 - -d(x,y) + d(y,z) >= d(x,z) -- 三角不等式 - -(1)根据三角不等式,则满足与 query 距离在 n 范围内的另一个字符转 B,其与 A的距离最大为 d+n,最小为 d-n。 - -(2)BK 树的构造就过程如下:每个节点有任意个子节点,每条边有个值表示编辑距离。所有子节点到父节点的边上标注 n 表示编辑距离恰好为 n。比如,我们有棵树父节点是”book”和两个子节点”cake”和”books”,”book”到”books”的边标号 1,”book”到”cake”的边上标号 4。从字典里构造好树后,无论何时你想插入新单词时,计算该单词与根节点的编辑距离,并且查找数值为d(neweord, root)的边。递归得与各子节点进行比较,直到没有子节点,你就可以创建新的子节点并将新单词保存在那。比如,插入”boo”到刚才上述例子的树中,我们先检查根节点,查找 d(“book”, “boo”) = 1 的边,然后检查标号为1 的边的子节点,得到单词”books”。我们再计算距离 d(“books”, “boo”)=2,则将新单词插在”books”之后,边标号为 2。 - -3、查询相似词如下:计算单词与根节点的编辑距离 d,然后递归查找每个子节点标号为 d-n 到 d+n(包含)的边。假如被检查的节点与搜索单词的距离 d 小于 n,则返回该节点并继续查询。比如输入 cape 且最大容忍距离为 1,则先计算和根的编辑距离 d(“book”, “cape”)=4,然后接着找和根节点之间编辑距离为 3 到5 的,这个就找到了 cake 这个节点,计算 d(“cake”, “cape”)=1,满足条件所以返回 cake,然后再找和 cake 节点编辑距离是 0 到 2 的,分别找到 cape 和cart 节点,这样就得到 cape 这个满足条件的结果。 - -![](https://user-gold-cdn.xitu.io/2019/12/26/16f412606760af54?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - - -## Linux篇 - - - -### 1、绝对路径用什么符号表示?当前目录、上层目录用什么表示?主目录用什么表示? 切换目录用什么命令? - -答案: - -绝对路径: 如/etc/init.d - -当前目录和上层目录: ./ ../ - -主目录: ~/ - -切换目录: cd - -### 2、怎么查看当前进程?怎么执行退出?怎么查看当前路径? - -答案: - -查看当前进程: ps - -执行退出: exit - -查看当前路径: pwd - -### 3、怎么清屏?怎么退出当前命令?怎么执行睡眠?怎么查看当前用户 id?查看指定帮助用什么命令? - -答案: - -清屏: clear - -退出当前命令: ctrl+c 彻底退出 - -执行睡眠 : ctrl+z 挂起当前进程 fg 恢复后台 - -查看当前用户 id: ”id“:查看显示目前登陆账户的 uid 和 gid 及所属分组及用户名 - -查看指定帮助: 如 man adduser 这个很全 而且有例子; adduser --help 这个告诉你一些常用参数; info adduesr; - -### 4、Ls 命令执行什么功能? 可以带哪些参数,有什么区别? - -答案: - -ls 执行的功能: 列出指定目录中的目录,以及文件 - -哪些参数以及区别: a 所有文件 l 详细信息,包括大小字节数,可读可写可执行的权限等 - -### 5、建立软链接(快捷方式),以及硬链接的命令。 - -答案: - -软链接: ln -s slink source - -硬链接: ln link source - -### 6、目录创建用什么命令?创建文件用什么命令?复制文件用什么命令? - -答案: - -创建目录: mkdir - -创建文件:典型的如 touch,vi 也可以创建文件,其实只要向一个不存在的文件输出,都会创建文件 - -复制文件: cp 7\. 文件权限修改用什么命令?格式是怎么样的? - -文件权限修改: chmod - -格式如下: - -chmodu+xfile 给 file 的属主增加执行权限 chmod 751 file 给 file 的属主分配读、写、执行(7)的权限,给 file 的所在组分配读、执行(5)的权限,给其他用户分配执行(1)的权限 - -chmodu=rwx,g=rx,o=xfile 上例的另一种形式 chmod =r file 为所有用户分配读权限 - -chmod444file 同上例 chmod a-wx,a+r file 同上例 - -$ chmod -R u+r directory 递归地给 directory 目录下所有文件和子目录的属主分配读的权限 - -### 7、查看文件内容有哪些命令可以使用? - -答案: - -vi 文件名 #编辑方式查看,可修改 - -cat 文件名 #显示全部文件内容 - -more 文件名 #分页显示文件内容 - -less 文件名 #与 more 相似,更好的是可以往前翻页 - -tail 文件名 #仅查看尾部,还可以指定行数 - -head 文件名 #仅查看头部,还可以指定行数 - -### 8、随意写文件命令?怎么向屏幕输出带空格的字符串,比如”hello world”? - -答案: - -写文件命令:vi - -向屏幕输出带空格的字符串:echo hello world - -### 9、终端是哪个文件夹下的哪个文件?黑洞文件是哪个文件夹下的哪个命令? - -答案: - -终端 /dev/tty - -黑洞文件 /dev/null - -### 10、移动文件用哪个命令?改名用哪个命令? - -答案: - -mv mv - -### 11、复制文件用哪个命令?如果需要连同文件夹一块复制呢?如果需要有提示功能呢? - -答案: - -cp cp -r ???? - -### 12、删除文件用哪个命令?如果需要连目录及目录下文件一块删除呢?删除空文件夹用什么命令? - -答案: - -rm rm -r rmdir - -### 13、Linux 下命令有哪几种可使用的通配符?分别代表什么含义? - -答案: - -“?”可替代单个字符。 - -“*”可替代任意多个字符。 - -方括号“[charset]”可替代 charset 集中的任何单个字符,如[a-z],[abABC] - -### 14、用什么命令对一个文件的内容进行统计?(行号、单词数、字节数) - -答案: - -wc 命令 - c 统计字节数 - l 统计行数 - w 统计字数。 - -### 15、Grep 命令有什么用? 如何忽略大小写? 如何查找不含该串的行? - -答案: - -是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹 配的行打印出来。 - -grep [stringSTRING] filename grep [^string] filename - -![](data:image/svg+xml;utf8,) - -### 16、Linux 中进程有哪几种状态?在 ps 显示出来的信息中,分别用什么符号表示的? - -答案: - -(1)不可中断状态:进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指进程不响应异步信号。 - -(2)暂停状态/跟踪状态:向进程发送一个 SIGSTOP 信号,它就会因响应该信号 而进入 TASK_STOPPED 状态;当进程正在被跟踪时,它处于 TASK_TRACED 这个特殊的状态。正被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。 - -(3)就绪状态:在 run_queue 队列里的状态 - -(4)运行状态:在 run_queue 队列里的状态 - -(5)可中断睡眠状态:处于这个状态的进程因为等待某某事件的发生(比如等待socket 连接、等待信号量),而被挂起 - -(6)zombie 状态(僵尸):父亲没有通过 wait 系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉 - -(7)退出状态 - -D 不可中断 Uninterruptible(usually IO) - -R 正在运行,或在队列中的进程 - -S 处于休眠状态 - -T 停止或被追踪 - -Z 僵尸进程 - -W 进入内存交换(从内核 2.6 开始无效) - -X 死掉的进程 - -### 17、怎么使一个命令在后台运行? - -答案: - -一般都是使用 & 在命令结尾来让程序自动运行。(命令后可以不追加空格) - -### 18、利用 ps 怎么显示所有的进程? 怎么利用 ps 查看指定进程的信息? - -答案: - -``` -ps -ef (system v 输出) -ps -aux bsd 格式输出 -ps -ef | grep pid复制代码 -``` - -### 19、哪个命令专门用来查看后台任务? - -答案: - -job -l - -### 20、把后台任务调到前台执行使用什么命令?把停下的后台任务在后台执行起来用什么命令? - -答案: - -把后台任务调到前台执行 fg - -把停下的后台任务在后台执行起来 bg - -### 21、终止进程用什么命令? 带什么参数? - -答案: - -kill [-s <信息名称或编号>][程序] 或 kill [-l <信息编号>] - -kill-9 pid - -### 22、怎么查看系统支持的所有信号? - -答案: - -kill -l - -### 23、搜索文件用什么命令? 格式是怎么样的? - -答案: - -find <指定目录> <指定条件> <指定动作> - -whereis 加参数与文件名 - -locate 只加文件名 - -find 直接搜索磁盘,较慢。 - -find / -name "string*" - -### 24、查看当前谁在使用该主机用什么命令? 查找自己所在的终端信息用什么命令? - -答案: - -查找自己所在的终端信息:who am i - -查看当前谁在使用该主机:who - -### 25、使用什么命令查看用过的命令列表? - -答案: - -history - -### 26、使用什么命令查看磁盘使用空间? 空闲空间呢? - -答案: - -``` -df -hl复制代码 -``` - -文件系统 容量 已用 可用 已用% 挂载点 - -``` -Filesystem Size Used Avail Use% Mounted on /dev/hda2 45G 19G 24G -44% / -/dev/hda1 494M 19M 450M 4% /boot复制代码 -``` - -### 27、使用什么命令查看网络是否连通? - -答案: - -netstat - -### 28、使用什么命令查看 ip 地址及接口信息? - -答案: - -ifconfig - -### 29、查看各类环境变量用什么命令? - -答案: - -查看所有 env - -查看某个,如 home: env $HOME - -### 30、通过什么命令指定命令提示符? - -答案: - -\u:显示当前用户账号 - -\h:显示当前主机名 - -\W:只显示当前路径最后一个目录 - -\w:显示当前绝对路径(当前用户目录会以~代替) - -$PWD:显示当前全路径 - -$:显示命令行’$'或者’#'符号 - - - -\d:代表日期,格式为 week day month date,例如:"MonAug1" - -\t:显示时间为 24 小时格式,如:HH:MM:SS - -\T:显示时间为 12 小时格式 - -\A:显示时间为 24 小时格式:HH:MM - -\v:BASH 的版本信息 如 export PS1=’[\u@\h\w#]$‘ - -### 31、查找命令的可执行文件是去哪查找的? 怎么对其进行设置及添加? - -答案: - -whereis [-bfmsu][-B <目录>...][-M <目录>...][-S <目录>...][文件...] - -补充说明:whereis 指令会在特定目录中查找符合条件的文件。这些文件的烈性应属于原始代码,二进制文件,或是帮助文件。 - --b 只查找二进制文件。 - --B <目录> 只在设置的目录下查找二进制文件。 -f 不显示文件名前的路径名称。 - --m 只查找说明文件。 - --M <目录> 只在设置的目录下查找说明文件。-s 只查找原始代码文件。 - --S <目录> 只在设置的目录下查找原始代码文件。 -u 查找不包含指定类型的文件。 - -w -h ich 指令会在 PATH 变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。 - --n 指定文件名长度,指定的长度必须大于或等于所有文件中最长的文件名。 - --p 与-n 参数相同,但此处的包括了文件的路径。 -w 指定输出时栏位的宽度。 - --V 显示版本信息 - -### 32、通过什么命令查找执行命令? - -答案: - -which 只能查可执行文件 - -whereis 只能查二进制文件、说明文档,源文件等 - -![](data:image/svg+xml;utf8,) - -### 33、怎么对命令进行取别名? - -答案: - -``` -alias la='ls -a'复制代码 -``` -### 34、du 和 df 的定义,以及区别? -答案: +**2. JRE** -du 显示目录或文件的大小 +JRE(Java Runtime Environment)**,Java运行时环境,用于解释执行Java的字节码文件**。普通用户只需要安装JRE来运行Java程序即可,而作为一名程序员必须安装JDK,来编译、调试程序。 -df 显示每个<文件>所在的文件系统的信息,默认是显示所有文件系统。(文件系统分配其中的一些磁盘块用来记录它自身的一些数据,如 i 节点,磁盘分布图,间接块,超级块等。这些数据对大多数用户级的程序来说是不可见的,通常称为 Meta Data。) du 命令是用户级的程序,它不考虑 Meta Data,而 df命令则查看文件系统的磁盘分配图并考虑 Meta Data。 -df 命令获得真正的文件系统数据,而 du 命令只查看文件系统的部分情况。 -### 35、awk 详解。 +**3. JVM** -答案: +JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。**它是整个Java实现跨平台的核心**,负责解释执行字节码文件,是可运行Java字节码文件的虚拟计算机。所有平台上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。 -``` -awk '{pattern + action}' { - filenames -} -#cat /etc/passwd |awk -F ':' '{print 1"t"7}' //-F 的意思是以':'分隔 root -/bin/bash -daemon /bin/sh 搜索/etc/passwd 有 root 关键字的所有行 -#awk -F: '/root/' /etc/passwd root:x:0:0:root:/root:/bin/bash复制代码 -``` +当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。也就是说**JVM是运行Java字节码的虚拟机。** -### 36、当你需要给命令绑定一个宏或者按键的时候,应该怎么做呢? +不同平台的JVM是不同的,但是他们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相同的虚拟机,编译后的Java字节码就可以在该平台上运行。 -答案: -可以使用 bind 命令,bind 可以很方便地在 shell 中实现宏或按键的绑定。在进行按键绑定的时候,我们需要先获取到绑定按键对应的字符序列。 -比如获取 F12 的字符序列获取方法如下:先按下 Ctrl+V,然后按下 F12 .我们就可以得到 F12 的字符序列 ^[[24~。 +**为什么要采用字节码:** -接着使用 bind 进行绑定。 +> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即Java源代码经过虚拟机编译器编译后扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 -``` -[root@localhost ~]# bind ‘”e[24~":"date"'复制代码 -``` -注意:相同的按键在不同的终端或终端模拟器下可能会产生不同的字符序列。 -【附】也可以使用 showkey -a 命令查看按键对应的字符序列。 +**什么是跨平台:** -### 37、如果一个 linux 新手想要知道当前系统支持的所有命令的列表,他需要怎么做? +> 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 +> +> 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 -答案: -使用命令 compgen -c,可以打印出所有支持的命令列表。 -``` -[root@localhost ~]$ compgen -c -l. -ll -ls -which -if -then else -elif -fi -case -esac -for -select -while -until -do -done -…复制代码 -``` +**Java 程序从源代码到运行需要三步:** -### 38、如果你的助手想要打印出当前的目录栈,你会建议他怎么做? +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210321015133.png) -答案: +**4. 总结** -使用 Linux 命令 dirs 可以将当前的目录栈打印出来。 +1. JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无序安装JDK。 +2. JDk包含JRE,JDK 和 JRE 中都包含 JVM。 +3. JVM 是 Java 编程语言的核心并且具有平台独立性。 -``` -[root@localhost ~]# dirs -/usr/share/X11复制代码 -``` +### 3. 什么是跨平台性?原理是什么 +* 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 -【附】:目录栈通过 pushd popd 来操作。 +* 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 -### 39、你的系统目前有许多正在运行的任务,在不重启机器的条件下,有什么方法可以把所有正在运行的进程移除呢? +### 4. Java程序是如何执行的 +我们日常的工作中都使用开发工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的调试程序,或者是通过打包工具把项目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常运行了,但你有没有想过 Java 程序内部是如何执行的?其实不论是在开发工具中运行还是在 Tomcat 中运行,Java 程序的执行流程基本都是相同的,它的执行流程如下: -答案: +- 先把 Java 代码编译成字节码,也就是把 .java 类型的文件编译成 .class 类型的文件。这个过程的大致执行流程:Java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字符码生成器 -> 最终生成字节码,其中任何一个节点执行失败就会造成编译失败; +- 把 class 文件放置到 Java 虚拟机,这个虚拟机通常指的是 Oracle 官方自带的 Hotspot JVM; +- Java 虚拟机使用类加载器(Class Loader)装载 class 文件; +- 类加载完成之后,会进行字节码效验,字节码效验通过之后 JVM 解释器会把字节码翻译成机器码交由操作系统执行。但不是所有代码都是解释执行的,JVM 对此做了优化,比如,以 Hotspot 虚拟机来说,它本身提供了 JIT(Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热点代码编译为机器码,这个时候字节码就变成了编译执行。Java 程序执行流程图如下: -使用 linux 命令 ’disown -r ’可以将所有正在运行的进程移除。 +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210324143756.png) -### 40、bash shell 中的 hash 命令有什么作用? +### 5. Java SE 、Java EE 、Java ME -答案: +### 6. Oracle JDK 和 OpenJDK 的对比 -linux 命令’hash’管理着一个内置的哈希表,记录了已执行过的命令的完整路径,用该命令可以打印出你所使用过的命令以及执行的次数。 +### 7. Java和C++的区别 -``` -[root@localhost ~]# hash -hits command -2 /bin/ls -2 /bin/su复制代码 +# 基础部分 +### 1. instanceof 关键字的作用 +instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为: + +```java +boolean result = obj instanceof Class ``` -### 41、哪一个 bash 内置命令能够进行数学运算。 +其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。 -答案: +注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。 -bash shell 的内置命令 let 可以进行整型数的数学运算。 +```java +int i = 0; +System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型 +System.out.println(i instanceof Object);//编译不通过 +``` +```java +Integer integer = new Integer(1); +System.out.println(integer instanceof Integer);//true ``` -#! /bin/bash -… -… -let c=a+b -… -…复制代码 + +```java +//false  ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。 +System.out.println(null instanceof Object); ``` -### 42、怎样一页一页地查看一个大文件的内容呢? +### 2. == 和 equals 的区别是什么? +**"=="** -答案: +对于基本类型和引用类型 == 的作用效果是不同的,如下所示: -通过管道将命令”cat file_name.txt” 和 ’more’ 连接在一起可以实现这个需要. +- 基本类型:比较的是值是否相同; +- 引用类型:比较的是引用是否相同; -``` -[root@localhost ~]# cat file_name.txt | more复制代码 +```java +String x = "string"; +String y = "string"; +String z = new String("string"); +System.out.println(x==y); // true +System.out.println(x==z); // false +System.out.println(x.equals(y)); // true +System.out.println(x.equals(z)); // true ``` -### 43、数据字典属于哪一个用户的? +> 因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。 -答案: -数据字典是属于’SYS’用户的,用户‘SYS’ 和 ’SYSEM’是由系统默认自动创建的 -### 44、怎样查看一个 linux 命令的概要与用法?假设你在/bin 目录中偶然看到一个你从没见过的的命令,怎样才能知道它的作用和用法呢? +**equals** -答案: +equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。 -使用命令 whatis 可以先出显示出这个命令的用法简要,比如,你可以使用 whatiszcat 去查看‘zcat’的介绍以及使用简要。 +首先来看默认情况下 equals 比较一个有相同值的对象,代码如下: -``` -[root@localhost ~]# whatis zcat -zcat [gzip] (1) – compress or expand files复制代码 -``` +```java +class Cat { + public Cat(String name) { + this.name = name; + } -### 45、使用哪一个命令可以查看自己文件系统的磁盘空间配额呢? + private String name; + + public String getName() { + return name; + } -答案: + public void setName(String name) { + this.name = name; + } +} -使用命令 repquota 能够显示出一个文件系统的配额信息 +Cat c1 = new Cat("叶痕秋"); +Cat c2 = new Cat("叶痕秋"); +System.out.println(c1.equals(c2)); // false +``` +输出结果出乎我们的意料,竟然是 false?这是怎么回事,看了 equals 源码就知道了,源码如下: +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` -### 46、列举几个常用的Linux命令 +原来 equals 本质上就是 ==。 -- 列出文件列表:ls【参数 -a -l】 -- 创建目录和移除目录:mkdir rmdir -- 用于显示文件后几行内容:tail,例如: tail -n 1000:显示最后1000行 -- 打包:tar -xvf -- 打包并压缩:tar -zcvf -- 查找字符串:grep -- 显示当前所在目录:pwd创建空文件:touch -- 编辑器:vim vi +那问题来了,两个相同值的 String 对象,为什么返回的是 true?代码如下: +```java +String s1 = new String("叶子"); +String s2 = new String("叶子"); +System.out.println(s1.equals(s2)); // true +``` +同样的,当我们进入 String 的 equals 方法,找到了答案,代码如下: -### 47、你平时是怎么查看日志的? +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` - Linux查看日志的命令有多种: tail、cat、tac、head、echo等,本文只介绍几种常用的方法。 +原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。 -**1、tail** +**总结** - 最常用的一种查看方式 +**== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。** - **命令格式: tail[必要参数][选择参数][文件]** +### 3. Hashcode的作用 +java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就会比较满。 --f 循环读取 --q 不显示处理信息 --v 显示详细的处理信息 --c<数目> 显示的字节数 --n<行数> 显示行数 --q, --quiet, --silent 从不输出给出文件名的首部 --s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒 +于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。 -例如: +hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。 -``` -tail -n 10 test.log 查询日志尾部最后10行的日志; -tail -n +10 test.log 查询10行之后的所有日志; -tail -fn 10 test.log 循环实时查看最后1000行记录(最常用的) -``` +### 4. 两个对象的 hashCode() 相同, 那么 equals() 也一定为 true吗? +不对,两个对象的 hashCode() 相同,equals() 不一定 true。 - 一般还会配合着grep搜索用,例如 : +代码示例: -``` -tail -fn 1000 test.log | grep '关键字' +```java +String str1 = "keep"; +String str2 = "brother"; +System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode())); +System. out. println(str1. equals(str2)); ``` - 如果一次性查询的数据量太大,可以进行翻页查看,例如: +执行结果: ``` -tail -n 4700 aa.log |more -1000 可以进行多屏显示(ctrl + f 或者 空格键可以快捷键) +str1:1179395 | str2:1179395 + +false ``` +代码解读:很显然“keep”和“brother”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。 +### 5. 泛型常用特点 +泛型是Java SE 1.5之后的特性, 《Java 核心技术》中对泛型的定义是: -**2、head** +> “泛型” 意味着编写的代码可以被不同类型的对象所重用。 - 跟tail是相反的head是看前多少行日志 +“泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素,如 -``` -head -n 10 test.log 查询日志文件中的头10行日志; -head -n -10 test.log 查询日志文件除了最后10行的其他所有日志; +```java +List iniData = new ArrayList<>() ``` - head其他参数参考tail +### 6. 使用泛型的好处? +以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。 -**3、cat** +### . 什么是java序列化,如何实现java序列化?或者请解释Serializable接口的作用。 -cat 是由第一行到最后一行连续显示在屏幕上 +# 数据类型 +### 1. Java有哪些数据类型 -一次显示整个文件 : +Java**中**有 8 种基本数据类型,分别为: -``` -$ cat filename -``` +- **6 种数字类型 (四个整数形,两个浮点型)**:byte、short、int、long、float、double - 从键盘创建一个文件 : +- **1 种字符类型**:char +- **1 种布尔型**:boolean。 -``` -$cat > filename -``` +**byte:** - 将几个文件合并为一个文件: +- byte 数据类型是8位、有符号的,以二进制补码表示的整数; +- 最小值是 **-128(-2^7)**; +- 最大值是 **127(2^7-1)**; +- 默认值是 **0**; +- byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一; +- 例子:byte a = 100,byte b = -50。 -``` -$cat file1 file2 > file 只能创建新文件,不能编辑已有文件 -``` +**short:** - 将一个日志文件的内容追加到另外一个 : +- short 数据类型是 16 位、有符号的以二进制补码表示的整数 +- 最小值是 **-32768(-2^15)**; +- 最大值是 **32767(2^15 - 1)**; +- Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一; +- 默认值是 **0**; +- 例子:short s = 1000,short r = -20000。 -``` -$cat -n textfile1 > textfile2 -``` +**int:** - 清空一个日志文件: +- int 数据类型是32位、有符号的以二进制补码表示的整数; +- 最小值是 **-2,147,483,648(-2^31)**; +- 最大值是 **2,147,483,647(2^31 - 1)**; +- 一般地整型变量默认为 int 类型; +- 默认值是 **0** ; +- 例子:int a = 100000, int b = -200000。 -``` -$cat : >textfile2 -``` +**long:** -注意:> 意思是创建,>>是追加。千万不要弄混了。 +- **注意:Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析** -cat其他参数参考tail +- long 数据类型是 64 位、有符号的以二进制补码表示的整数; +- 最小值是 **-9,223,372,036,854,775,808(-2^63)**; +- 最大值是 **9,223,372,036,854,775,807(2^63 -1)**; +- 这种类型主要使用在需要比较大整数的系统上; +- 默认值是 **0L**; +- 例子: long a = 100000L,Long b = -200000L。 + "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。 +**float:** +- float 数据类型是单精度、32位、符合IEEE 754标准的浮点数; +- float 在储存大型浮点数组的时候可节省内存空间; +- 默认值是 **0.0f**; +- 浮点数不能用来表示精确的值,如货币; +- 例子:float f1 = 234.5f。 -**4、more** +**double:** -more命令是一个基于vi编辑器文本过滤器,它以全屏幕的方式按页显示文本文件的内容,支持vi中的关键字定位操作。more名单中内置了若干快捷键,常用的有H(获得帮助信息),Enter(向下翻滚一行),空格(向下滚动一屏),Q(退出命令)。more命令从前向后读取文件,因此在启动时就加载整个文件。 +- double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数; +- 浮点数的默认类型为double类型; +- double类型同样不能表示精确的值,如货币; +- 默认值是 **0.0d**; +- 例子:double d1 = 123.4。 -该命令一次显示一屏文本,满屏后停下来,并且在屏幕的底部出现一个提示信息,给出至今己显示的该文件的百分比:–More–(XX%) +**char:** -- more的语法:more 文件名 -- Enter 向下n行,需要定义,默认为1行 -- Ctrl f 向下滚动一屏 -- 空格键 向下滚动一屏 -- Ctrl b 返回上一屏 -- = 输出当前行的行号 -- :f 输出文件名和当前行的行号 -- v 调用vi编辑器 -- !命令 调用Shell,并执行命令 -- q退出more +- char类型是一个单一的 16 位 Unicode 字符; +- 最小值是 **\u0000**(即为 0); +- 最大值是 **\uffff**(即为 65535); +- char 数据类型可以储存任何字符; +- 例子:char letter = 'A';(**单引号**) +**boolean:** +- boolean数据类型表示一位的信息; +- 只有两个取值:true 和 false; +- 这种类型只作为一种标志来记录 true/false 情况; +- 默认值是 **false**; +- 例子:boolean one = true。 -**5、sed** +**这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean** -这个命令可以查找日志文件特定的一段 , 根据时间的一个范围查询,可以按照行号和时间范围查询 +| 类型名称 | 字节、位数 | 最小值 | 最大值 | 默认值 | 例子 | +| :----------- | :-------------------- | :--------------------------------------------- | :------------------------------------- | :--------------- | :---------------------------------- | +| byte字节 | 1字节,8位 | -128(-2^7) | 127(2^7-1) | 0 | byte a = 100,byte b = -50 | +| short短整型 | 2字节,16位 | -32768(-2^15) | 32767(2^15 - 1) | 0 | short s = 1000,short r = -20000 | +| int整形 | 4字节,32位 | -2,147,483,648(-2^31) | 2,147,483,647(2^31 - 1) | 0 | int a = 100000, int b = -200000 | +| lang长整型 | 8字节,64位 | -9,223,372,036,854,775,808(-2^63) | 9,223,372,036,854,775,807(2^63 -1) | 0L | long a = 100000L,Long b = -200000L | +| double双精度 | 8字节,64位 | | double类型同样不能表示精确的值,如货币 | 0.0d | double d1 = 123.4 | +| float单精度 | 4字节,32位 | 在储存大型浮点数组的时候可节省内存空间 | 不同统计精准的货币值 | 0.0f | float f1 = 234.5f | +| char字符 | 2字节,16位 | \u0000(即为0) | \uffff(即为65,535) | 可以储存任何字符 | char letter = 'A'; | +| boolean布尔 | 返回true和false两个值 | 这种类型只作为一种标志来记录 true/false 情况; | 只有两个取值:true 和 false; | false | boolean one = true | -按照行号 +### 2. Java中引用数据类型有哪些,它们与基本数据类型有什么区别? +引用数据类型分3种:类,接口,数组; -``` -sed -n '5,10p' filename 这样你就可以只查看文件的第5行到第10行。 -``` +**简单来说,只要不是基本数据类型.都是引用数据类型。 那他们有什么不同呢?** - 按照时间段 -``` -sed -n '/2014-12-17 16:17:20/,/2014-12-17 16:17:36/p' test.log -``` +**1、从概念方面来说** +1,基本数据类型:变量名指向具体的数值 -**6、less** +2,引用数据类型:变量名不是指向具体的数值,而是指向存数据的内存地址,.也及时hash值 - less命令在查询日志时,一般流程是这样的 -``` -less log.log -shift + G 命令到文件尾部 然后输入 ?加上你要搜索的关键字例如 ?1213 +**2、从内存的构建方面来说**(内存中,有堆内存和栈内存两者) -按 n 向上查找关键字 +1,基本数据类型:被创建时,在栈内存中会被划分出一定的内存,并将数值存储在该内存中. -shift+n 反向查找关键字 -less与more类似,使用less可以随意浏览文件,而more仅能向前移动,不能向后移动,而且 less 在查看之前不会加载整个文件。 -less log2013.log 查看文件 -ps -ef | less ps查看进程信息并通过less分页显示 -history | less 查看命令历史使用记录并通过less分页显示 -less log2013.log log2014.log 浏览多个文件 -``` +2,引用数据类型:被创建时,首先会在栈内存中分配一块空间,然后在堆内存中也会分配一块具体的空间用来存储数据的具体信息,即hash值,然后由栈中引用指向堆中的对象地址. - 常用命令参数: -``` -less与more类似,使用less可以随意浏览文件,而more仅能向前移动,不能向后移动,而且 less 在查看之前不会加载整个文件。 -less log2013.log 查看文件 -ps -ef | less ps查看进程信息并通过less分页显示 -history | less 查看命令历史使用记录并通过less分页显示 -less log2013.log log2014.log 浏览多个文件 -常用命令参数: --b <缓冲区大小> 设置缓冲区的大小 --g 只标志最后搜索的关键词 --i 忽略搜索时的大小写 --m 显示类似more命令的百分比 --N 显示每行的行号 --o <文件名> 将less 输出的内容在指定文件中保存起来 --Q 不使用警告音 --s 显示连续空行为一行 -/字符串:向下搜索"字符串"的功能 -?字符串:向上搜索"字符串"的功能 -n:重复前一个搜索(与 / 或 ? 有关) -N:反向重复前一个搜索(与 / 或 ? 有关) -b 向后翻一页 -h 显示帮助界面 -q 退出less 命令 -``` - 一般本人查日志配合应用的其他命令 +**举个例子** +```java +//基本数据类型作为方法参数被调用 +public class Main{ + public static void main(String[] args){ + //基本数据类型 + int i = 1; + int j = 1; + double d = 1.2; + + //引用数据类型 + String str = "Hello"; + String str1= "Hello"; + } +} ``` -history // 所有的历史记录 -history | grep XXX // 历史记录中包含某些指令的记录 -history | more // 分页查看记录 -history -c // 清空所有的历史记录 +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210322011647.png) -!! 重复执行上一个命令 +由上图可知,基本数据类型中会存在两个相同的1,而引用型类型就不会存在相同的数据。 +假如"hello"的引用地址是xxxxx1,声明str变量并其赋值"hello"实际上就是让str变量引用了"hello"的内存地址,这个内存地址就存储在堆内存中,是不会改变的,当再次声明变量str1也是赋值为"hello"时,此时就会在堆内存中查询是否有"hello"这个地址,如果堆内存中已经存在这个地址了,就不会再次创建了,而是让str1变量也指向xxxxx1这个地址,如果没有的话,就会重新创建一个地址给str1变量。 -查询出来记录后选中 : !323 -``` +**从使用方面来说** +1,基本数据类型:判断数据是否相等,用==和!=判断。 +2,引用数据类型:判断数据是否相等,用equals()方法,==和!=是比较数值的。而equals()方法是比较内存地址的。 +**补充:数据类型选择的原则** +- 如果要表示整数就使用int,表示小数就使用double; +- 如果要描述日期时间数字或者表示文件(或内存)大小用long; +- 如果要实现内容传递或者编码转换使用byte; +- 如果要实现逻辑的控制,可以使用booleam; +- 如果要使用中文,使用char避免中文乱码; +- 如果按照保存范围:byte < int < long < double; -## 数据结构与算法篇 -### 1、常用的数据结构 +### 3. 自动装箱与拆箱 +**什么是自动装箱拆箱?** -> 原文:[The top data structures you should know for your next coding interview](https://medium.freecodecamp.org/the-top-data-structures-you-should-know-for-your-next-coding-interview-36af0831f5e3) -> -> 译者:[Fundebug](https://www.fundebug.com/) +从下面的代码中就可以看到装箱和拆箱的过程 + +```java +//自动装箱 +Integer total = 99; + +//自定拆箱 +int totalprim = total; +``` -我们首先列出最常用的数据结构 +**装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。** -- 数组 -- 堆栈 -- 队列 -- 链表 -- 树 -- 图 -- 字典树 -- 哈希表 +> 在Java SE5之前,自动装箱要这样写:Integer i = ``new` `Integer(``10``); -##### 1. 数组 -**数组(Array)**大概是最简单,也是最常用的数据结构了。其他数据结构,比如栈和队列都是由数组衍生出来的。 -下图展示了 1 个数组,它有 4 个元素: +对于Java的自动装箱和拆箱,我们看看源码编译后的class文件,其实装箱调用包装类的valueOf方法,拆箱调用的是Integer.Value方法,下面就是变编译后的代码: - ![img](https://image.fundebug.com/2019-03-27-array.png) +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210322151715.png) -每一个数组元素的位置由数字编号,称为下标或者索引(index)。大多数编程语言的数组第一个元素的下标是 0。 -根据维度区分,有 2 种不同的数组: -- 一维数组(如上图所示) -- 多维数组(数组的元素为数组) +**常见面试一:** -**数组的基本操作** +这段代码输出什么? -- Insert - 在某个索引处插入元素 -- Get - 读取某个索引处的元素 -- Delete - 删除某个索引处的元素 -- Size - 获取数组的长度 +```java +public class Main { + public static void main(String[] args) { + + Integer i1 = 100; + Integer i2 = 100; + Integer i3 = 200; + Integer i4 = 200; + + System.out.println(i1==i2); + System.out.println(i3==i4); + } +} +``` -**常见数组代码面试题** +答案是: -- [查找数组中第二小的元素](https://www.geeksforgeeks.org/to-find-smallest-and-second-smallest-element-in-an-array/):https://www.geeksforgeeks.org/to-find-smallest-and-second-smallest-element-in-an-array/ -- [查找第一个没有重复的数组元素](https://www.geeksforgeeks.org/non-repeating-element/): https://www.geeksforgeeks.org/non-repeating-element/ -- [合并 2 个排序好的数组](https://www.geeksforgeeks.org/merge-two-sorted-arrays/):https://www.geeksforgeeks.org/merge-two-sorted-arrays/ -- [重新排列数组中的正数和负数](https://www.geeksforgeeks.org/rearrange-positive-and-negative-numbers-publish/): https://www.geeksforgeeks.org/rearrange-positive-and-negative-numbers-publish/ +``` +true +false +``` -##### 2. 栈 +为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现: -撤回,即 Ctrl+Z,是我们最常见的操作之一,大多数应用都会支持这个功能。你知道它是怎么实现的吗?答案是这样的:把之前的应用状态(限制个数)保存到内存中,最近的状态放到第一个。这时,我们需要**栈(stack)**来实现这个功能。 +```java +public static Integer valueOf(int i) { + if(i >= -128 && i <= IntegerCache.high) + return IntegerCache.cache[i + 128]; + else + return new Integer(i); + } +``` -栈中的元素采用 LIFO (Last In First Out),即**后进先出**。 -下图的栈有 3 个元素,3 在最上面,因此它会被第一个移除: -![img](https://image.fundebug.com/2019-03-27-stack.png) +```java +private static class IntegerCache { + static final int high; + static final Integer cache[]; -**栈的基本操作** + static { + final int low = -128; -- Push —  在栈的最上方插入元素 -- Pop — 返回栈最上方的元素,并将其删除 -- isEmpty —  查询栈是否为空 -- Top —  返回栈最上方的元素,并不删除 + // high value may be configured by property + int h = 127; + if (integerCacheHighPropValue != null) { + // Use Long.decode here to avoid invoking methods that + // require Integer's autoboxing cache to be initialized + int i = Long.decode(integerCacheHighPropValue).intValue(); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - -low); + } + high = h; -**常见的栈代码面试题** + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + } -- [使用栈计算后缀表达式](https://www.geeksforgeeks.org/stack-set-4-evaluation-postfix-expression/):https://www.geeksforgeeks.org/stack-set-4-evaluation-postfix-expression/ -- [使用栈为栈中的元素排序](https://www.geeksforgeeks.org/sort-stack-using-temporary-stack/):https://www.geeksforgeeks.org/sort-stack-using-temporary-stack/ -- [检查字符串中的括号是否匹配正确](https://www.geeksforgeeks.org/check-for-balanced-parentheses-in-an-expression/):https://www.geeksforgeeks.org/check-for-balanced-parentheses-in-an-expression/ + private IntegerCache() {} + } +``` -##### 3. 队列 -**队列(Queue)**与栈类似,都是采用线性结构存储数据。它们的区别在于,栈采用 LIFO 方式,而队列采用先进先出,即**FIFO(First in First Out)**。 -下图展示了一个队列,1 是最上面的元素,它会被第一个移除: +从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。 -![img](https://image.fundebug.com/2019-03-27-queue.png) +上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。 -**队列的基本操作** -- Enqueue —  在队列末尾插入元素 -- Dequeue —  将队列第一个元素删除 -- isEmpty —  查询队列是否为空 -- Top —  返回队列的第一个元素 -**常见的队列代码面试题** +**常见面试二:** -- [使用队列实现栈](https://www.geeksforgeeks.org/implement-stack-using-queue/):https://www.geeksforgeeks.org/implement-stack-using-queue/ -- [倒转队列的前 K 个元素](https://www.geeksforgeeks.org/reversing-first-k-elements-queue/):https://www.geeksforgeeks.org/reversing-first-k-elements-queue/ -- [使用队列将 1 到 n 转换为二进制](https://www.geeksforgeeks.org/interesting-method-generate-binary-numbers-1-n/): https://www.geeksforgeeks.org/interesting-method-generate-binary-numbers-1-n/ +```java +public class Main { + public static void main(String[] args) { + + Double i1 = 100.0; + Double i2 = 100.0; + Double i3 = 200.0; + Double i4 = 200.0; + + System.out.println(i1==i2); + System.out.println(i3==i4); + } +} +``` -##### 4. 链表 +输出结果为: -**链表(Linked List)**也是线性结构,它与数组看起来非常像,但是它们的内存分配方式、内部结构和插入删除操作方式都不一样。 +``` +false +false +``` -链表是一系列节点组成的链,每一个节点保存了数据以及指向下一个节点的指针。链表头指针指向第一个节点,如果链表为空,则头指针为空或者为 null。 +原因很简单,在某个范围内的整型数值的个数是有限的,而浮点数却不是。 -链表可以用来实现文件系统、哈希表和邻接表。 -下图展示了一个链表,它有 3 个节点: -![img](https://image.fundebug.com/2019-03-27-linked_list.png) +### 4. int 和 Integer 有什么区别 +* Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。 -链表分为 2 种: +* Java 为每个原始类型提供了包装类型: -- 单向链表 -- 双向链表 + * 原始类型: boolean,char,byte,short,int,long,float,double -**链表的基本操作** + * 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double -- InsertAtEnd —  在链表结尾插入元素 -- InsertAtHead —  在链表开头插入元素 -- Delete —  删除链表的指定元素 -- DeleteAtHead —  删除链表第一个元素 -- Search —  在链表中查询指定元素 -- isEmpty —  查询链表是否为空 -**常见的队列代码面试题** +### 4. Integer a= 127 与 Integer b = 127相等吗 +* 对于对象引用类型:==比较的是对象的内存地址。 +* 对于基本数据类型:==比较的是值。 -- [倒转 1 个链表](https://www.geeksforgeeks.org/reverse-a-linked-list/):https://www.geeksforgeeks.org/reverse-a-linked-list/ -- [检查链表中是否存在循环](https://www.geeksforgeeks.org/detect-loop-in-a-linked-list/):https://www.geeksforgeeks.org/detect-loop-in-a-linked-list/ -- [返回链表倒数第 N 个元素](https://www.geeksforgeeks.org/nth-node-from-the-end-of-a-linked-list/):https://www.geeksforgeeks.org/nth-node-from-the-end-of-a-linked-list/ -- [移除链表中的重复元素](https://www.geeksforgeeks.org/remove-duplicates-from-an-unsorted-linked-list/):https://www.geeksforgeeks.org/remove-duplicates-from-an-unsorted-linked-list/ +`如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false` -##### 5. 图 +```java +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比较 + System.out.println(b == c); // true -**图(graph)**由多个**节点(vertex)**构成,节点之间阔以互相连接组成一个网络。(x, y)表示一条**边(edge)**,它表示节点 x 与 y 相连。边可能会有**权值(weight/cost)**。 + Integer a1 = 128; + Integer b1 = 128; + System.out.println(a1 == b1); // false -![img](https://image.fundebug.com/2019-03-27-graph.png) + Integer a2 = 127; + Integer b2 = 127; + System.out.println(a2 == b2); // true +} +``` -**图分为两种:** +### 5. 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);其中有隐含的强制类型转换。 -**在编程语言中,图有可能有以下两种形式表示:** +### Java的四种引用,强弱软虚 -- 邻接矩阵(Adjacency Matrix) -- 邻接表(Adjacency List) +# 常见关键字 +### 1. final 在 Java 中有什么作用? +final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。 -**遍历图有两周算法** +特征:凡是引用final关键字的地方皆不可修改! -- 广度优先搜索(Breadth First Search) -- 深度优先搜索(Depth First Search) +(1)修饰类:表示该类不能被继承; -**常见的图代码面试题** +(2)修饰方法:表示方法不能被重写; -- [实现广度优先搜索](https://www.geeksforgeeks.org/breadth-first-search-or-bfs-for-a-graph/): https://www.geeksforgeeks.org/breadth-first-search-or-bfs-for-a-graph/ -- [实现深度优先搜索](https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/): https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/ -- [检查图是否为树](https://www.geeksforgeeks.org/check-given-graph-tree/): https://www.geeksforgeeks.org/check-given-graph-tree/ -- [统计图中边的个数](https://www.geeksforgeeks.org/count-number-edges-undirected-graph/):https://www.geeksforgeeks.org/count-number-edges-undirected-graph/ -- [使用 Dijkstra 算法查找两个节点之间的最短距离](https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/): https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/ +(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。 +### 2.访问修饰符 public,private,protected,以及不写(默认)时的区别 +* **定义**:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。 +* **分类** -##### 6. 树 + * **private** : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) -**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成。树是一种特殊的图,它与图最大的区别是没有循环。 + * **default** (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。 -树被广泛应用在人工智能和一些复杂算法中,用来提供高效的存储结构。 + * **protected** : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 -下图是一个简单的树以及与树相关的术语: + * **public** : 对所有类可见。使用对象:类、接口、变量、方法 -![img](https://image.fundebug.com/2019-03-27-tree.png) +**访问修饰符图** -**树有很多分类:** +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/14/171744c433bcfd38?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) -- N 叉树(N-ary Tree) -- 平衡树(Balanced Tree) -- 二叉树(Binary Tree) -- 二叉查找树(Binary Search Tree) -- 平衡二叉树(AVL Tree) -- 红黑树(Red Black Tree) -- 2-3 树(2–3 Tree) +### 3. final 有什么用? +用于修饰类、属性和方法 -其中,二叉树和二叉查找树是最常用的树。 +* 被final修饰的类不可以被继承 +* 被final修饰的方法不可以被重写 +* 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的 -**常见的树代码面试题** +### 4. final有哪些用法? +final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了: -- [计算树的高度](https://www.geeksforgeeks.org/write-a-c-program-to-find-the-maximum-depth-or-height-of-a-tree/):https://www.geeksforgeeks.org/write-a-c-program-to-find-the-maximum-depth-or-height-of-a-tree/ -- [查找二叉平衡树中第 K 大的元素](https://www.geeksforgeeks.org/kth-largest-element-in-bst-when-modification-to-bst-is-not-allowed/):https://www.geeksforgeeks.org/kth-largest-element-in-bst-when-modification-to-bst-is-not-allowed/ -- [查找树中与根节点距离为 k 的节点](https://www.geeksforgeeks.org/print-nodes-at-k-distance-from-root/):https://www.geeksforgeeks.org/print-nodes-at-k-distance-from-root/ -- [查找二叉树中某个节点所有祖先节点](https://www.geeksforgeeks.org/print-ancestors-of-a-given-node-in-binary-tree/):https://www.geeksforgeeks.org/print-ancestors-of-a-given-node-in-binary-tree/ +- 被final修饰的类不可以被继承 +- 被final修饰的方法不可以被重写 +- 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变. +- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率 +- 被final修饰的常量,在编译阶段会存入常量池中. -##### 7. 前缀树 +除此之外,编译器对final域要遵守的两个重排序规则更好: -**前缀树(Prefix Trees 或者 Trie)**与树类似,用于处理字符串相关的问题时非常高效。它可以实现快速检索,常用于字典中的单词查询,搜索引擎的自动补全甚至 IP 路由。 +在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序. -下图展示了“top”, “thus”和“their”三个单词在前缀树中如何存储的: +### 5. static都有哪些用法? +所有的人都知道static关键字这两个基本的用法:静态变量和静态方法.也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享. -![img](https://image.fundebug.com/2019-03-27-tries.png) +除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作: -单词是按照字母从上往下存储,“p”, “s”和“r”节点分别表示“top”, “thus”和“their”的单词结尾。 +```java +public calss PreCache{ + static{ + //执行相关操作 + } +} +``` -**常见的树代码面试题** +此外static也多用于修饰内部类,此时称之为静态内部类. -- [统计前缀树表示的单词个数](https://www.geeksforgeeks.org/counting-number-words-trie/):https://www.geeksforgeeks.org/counting-number-words-trie/ -- [使用前缀树为字符串数组排序](https://www.geeksforgeeks.org/sorting-array-strings-words-using-trie/): https://www.geeksforgeeks.org/sorting-array-strings-words-using-trie/ +最后一种用法就是静态导包,即`import static`.import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如: -##### 8. 哈希表 +```java +import static java.lang.Math.*; + +public class Test{ + + public static void main(String[] args){ + //System.out.println(Math.sin(20));传统做法 + System.out.println(sin(20)); + } +} +``` -**哈希(Hash)**将某个对象变换为唯一标识符,该标识符通常用一个短的随机字母和数字组成的字符串来代表。哈希可以用来实现各种数据结构,其中最常用的就是**哈希表(hash table)**。 +### 6. static和final区别 -哈希表通常由数组实现。 +| 关键词 | 修饰物 | 影响 | +| :----- | :----- | :------------------------------------------------------- | +| final | 变量 | 分配到常量池中,程序不可改变其值 | +| final | 方法 | 子类中将不能被重写 | +| final | 类 | 不能被继承 | +| static | 变量 | 分配在内存堆上,引用都会指向这一个地址而不会重新分配内存 | +| static | 方法块 | 虚拟机优先加载 | +| static | 类 | 可以直接通过类来调用而不需要new | -哈希表的性能取决于 3 个指标: -- 哈希函数 -- 哈希表的大小 -- 哈希冲突处理方式 +### 7. this关键字的用法 +* this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。 -下图展示了有数组实现的哈希表,数组的下标即为哈希值,由哈希函数计算,作为哈希表的**键(key)**,而数组中保存的数据即为**值(value)**: +* this的用法在java中大体可以分为3种: -![img](https://image.fundebug.com/2019-03-27-hash_table.png) + * 1.普通的直接引用,this相当于是指向当前对象本身。 -**常见的哈希表代码面试题** + * 2.形参与成员名字重名,用this来区分: -- [查找数组中对称的组合](https://www.geeksforgeseks.org/given-an-array-of-pairs-find-all-symmetric-pairs-in-it/): https://www.geeksforgeseks.org/given-an-array-of-pairs-find-all-symmetric-pairs-in-it/ -- [确认某个数组的元素是否为另一个数组元素的子集](https://www.geeksforgeeks.org/find-whether-an-array-is-subset-of-another-array-set-1/): https://www.geeksforgeeks.org/find-whether-an-array-is-subset-of-another-array-set-1/ -- [确认给定的数组是否互斥](https://www.geeksforgeeks.org/check-two-given-sets-disjoint/): https://www.geeksforgeeks.org/check-two-given-sets-disjoint/ + ```java + public Person(String name, int age) { + this.name = name; + this.age = age; + } + ``` + * 3.引用本类的构造函数 + ```java + class Person{ + private String name; + private int age; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + public Person(String name, int age) { + this(name); + this.age = age; + } + } + ``` -### 2、 数据里有{1,2,3,4,5,6,7,8,9},请随机打乱顺序,生成一个新的数组(请以代码实现) +### 8. super关键字的用法 +super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 -```java -import java.util.Arrays; - -//打乱数组 -public class Demo1 { - - //随机打乱 - public static int[] srand(int[] a) { - int[] b = new int[a.length]; - - for(int i = 0; i < a.length;i++) { - //随机获取下标 - int tmp = (int)(Math.random()*(a.length - i)); //随机数:[ 0 ,a.length - i ) - b[i] = a[tmp]; - - //将此时a[tmp]的下标移动到靠后的位置 - int change = a[a.length - i - 1]; - a[a.length - i - 1] = a[tmp]; - a[tmp] = change; - } - - return b; - } - - public static void main(String[] args) { - int[] a = {1,2,3,4,5,6,7,8,9}; - System.out.println(Arrays.toString(srand(a))); - } -} +**super也有三种用法:** -``` +* 1.普通的直接引用 +与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 +* 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 -### 3、 写出代码判断一个整数是不是2的阶次方(请代码实现,谢绝调用API方法) + ```java + class Person{ + protected String name; -```java -import java.util.Scanner; - -//判断整数是不是2的阶次方 -public class Demo2 { - - public static boolean check(int sum) { - boolean flag = true; //判断标志 - while(sum > 1) { - if (sum % 2 == 0) { - sum = sum/2; - } else { - flag = false; - break; + public Person(String name) { + this.name = name; } + } - return flag; - } - - public static void main(String[] args) { - Scanner scanner = new Scanner(System.in); - System.out.println("请输入一个整数:"); - int sum = scanner.nextInt(); - System.out.println(sum + " 是不是2的阶次方:" + check(sum)); - } -} -``` + class Student extends Person{ + private String name; + + public Student(String name, String name1) { + super(name); + this.name = name1; + } + public void getInfo(){ + System.out.println(this.name); //Child + System.out.println(super.name); //Father + } + } -### 4、 假设今日是2015年3月1日,星期日,请算出13个月零6天后是星期几,距离现在多少天(请用代码实现,谢绝调用API方法) + public class Test { + public static void main(String[] args) { + Student s1 = new Student("Father","Child"); + s1.getInfo(); -```java -import java.util.Scanner; - -//算出星期几 -public class Demo4 { - public static String[] week = {"星期日","星期一","星期二","星期三","星期四","星期五","星期六"}; - public static int i = 0; - public static int[] monthday1 = {0,31,28,31,30,31,30,31,31,30,31,30,31}; - public static int[] monthday2 = {0,31,29,31,30,31,30,31,31,30,31,30,31}; - - //查看距离当前天数的差值 - public static String distance(int year,int month,int day,int newMonth,int newDay) { - int sum = 0; //设定初始距离天数 - if (month + newMonth >= 12) { - if (((year + 1) % 4 == 0 && (year + 1) % 100 != 0)||(year + 1) % 400 == 0) { - sum += 366 + newDay; - for(int i = 0;i < newMonth - 12;i++) { - sum += monthday1[month + i]; - } - } else { - sum += 365 + newDay; - for(int i = 0;i < newMonth - 12;i++) { - sum += monthday1[month + i]; - } } - } else { - for(int i = 0;i < newMonth;i++) { - sum += monthday1[month + i]; - } - sum += newDay; } - return week[sum%7]; - } - - public static void main(String[] args) { - Scanner scanner = new Scanner(System.in); - System.out.println("请输入当前年份"); - int year = scanner.nextInt(); - System.out.println("请输入当前月份"); - int month = scanner.nextInt(); - System.out.println("请输入当前天数"); - int day = scanner.nextInt(); - System.out.println("请输入当前是星期几:以数字表示,如:星期天 为 0"); - int index = scanner.nextInt(); - System.out.println("今天是:" + year + "-" + month + "-" + day + " " + week[index]); - - System.err.println("请输入相隔月份"); - int newMonth = scanner.nextInt(); - System.out.println("请输入剩余天数"); - int newDay = scanner.nextInt(); - - System.out.println("经过" + newMonth + "月" + newDay + "天后,是" + distance(year,month,day,newMonth,newDay)); - } -} -``` + ``` +3.引用父类构造函数 +* super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 +* this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 -### 5、 有两个篮子,分别为A 和 B,篮子A里装有鸡蛋,篮子B里装有苹果,请用面向对象的思想实现两个篮子里的物品交换(请用代码实现) -```java -//面向对象思想实现篮子物品交换 -public class Demo5 { - public static void main(String[] args) { - //创建篮子 - Basket A = new Basket("A"); - Basket B = new Basket("B"); - - //装载物品 - A.load("鸡蛋"); - B.load("苹果"); - - //交换物品 - A.change(B); - - A.show(); - B.show(); - } -} +### 9. &和&&的区别 -class Basket{ - public String name; //篮子名称 - private Goods goods; //篮子中所装物品 - - public Basket(String name) { - // TODO Auto-generated constructor stub - this.name = name; - System.out.println(name + "篮子被创建"); - } - - //装物品函数 - public void load(String name) { - goods = new Goods(name); - System.out.println(this.name + "装载了" + name + "物品"); - } - - public void change(Basket B) { - System.out.println(this.name + " 和 " + B.name + "中的物品发生了交换"); - String tmp = this.goods.getName(); - this.goods.setName(B.goods.getName()); - B.goods.setName(tmp); - } - - public void show() { - System.out.println(this.name + "中有" + goods.getName() + "物品"); - } -} +* &运算符有两种用法:(1)按位与;(2)逻辑与。 -class Goods{ - private String name; //物品名称 - - public String getName() { - return name; - } +* &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 - public void setName(String name) { - this.name = name; - } +`注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。` - public Goods(String name) { - // TODO Auto-generated constructor stub - this.name = name; - } -} -``` +### 10. break ,continue ,return 的区别及作用 +* break 跳出总上一层循环,不再执行循环(结束当前的循环体) +* continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) +* return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) -### 6、更多算法练习 +# 异常处理 +### 1. Java异常简介 -**更多算法练习题,请访问 https://leetcode-cn.com/problemset/algorithms/** +Java异常是Java提供的一种识别及响应错误的一致性机制。 +Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。 +### 2. Java异常架构 -## 简历篇 +![image-20210814134319791](https://gitee.com/gsjqwyl/images/raw/master/uPic/tu731M.jpg) -> 原文: https://www.cnblogs.com/QQ12538552/p/12332620.html +### 3. Error 和 Exception 有什么区别? -本篇文章除了教大家用Markdown如何写一份程序员专属的简历,后面还会给大家推荐一些不错的用来写Markdown简历的软件或者网站,以及如何优雅的将Markdown格式转变为PDF格式或者其他格式。 +**Error ** -推荐大家使用Markdown语法写简历,然后再将Markdown格式转换为PDF格式后进行简历投递。 +表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况; -如果你对Markdown语法不太了解的话,可以花半个小时简单看一下Markdown语法说明: [http://www.markdown.cn](http://www.markdown.cn/) 。 +**Exception ** +表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。 -### 为什么说简历很重要? - 一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。 在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。为什么说简历很重要呢? +### 4. 阐述 final、finally、finalize 的区别 -### 先从面试来说 +**1、final:** 修饰符(关键字)有三种用法:如果一个类被声明为 final,意味着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。将变量声明为 final,可以保证它们在使用中不被改变,被声明为 final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为 final 的方法也同样只能使用,不能在子类中被重写。 -假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 +**2、finally:** 通常放在 try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以将释放外部资源的代码写在 finally 块中. -假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 +**3、finalize:** Object 类中定义的方法,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写 finalize()方法可以整理系统资源或者执行其他清理工作。 -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 +### 5. 列出一些你常见的运行时异常? -所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。 +### 6. 什么是受检异常 +### 7. Excption与Error包结构 +更多异常面试题及答案,都整理成了PDF: [完整版Java面试高清PDF合集](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md) -### 再从面试说起 +# Java集合 & 容器 +### 什么是集合 -我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。 +* 集合就是一个放数据的容器,准确的说是放数据对象引用的容器 -所以,首先,你要明确的一点是:你不会的东西就不要写在简历上。另外,你要考虑你该如何才能让你的亮点在简历中凸显出来,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个项目里使用了什么技术后整体性能和并发量提升了很多等等。 +* 集合类存放的都是对象的引用,而不是对象的本身 -面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。 +* 集合类型主要有3种:set(集)、list(列表)和map(映射)。 -### 必知必会的几点 -大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。 +### 集合的特点 +* 集合的特点主要有如下两点: +* 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。 -大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的工作 +* 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小 -写在简历上的东西一定要慎重,这是面试官大量提问的地方; -将自己的项目经历完美的展示出来非常重要。 +### 集合和数组的区别 +* 数组是固定长度的;集合可变长度的。 +* 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。 -### 必须了解的两大法则 +* 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 -**STAR法则(Situation Task Action Result)** -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 +### 使用集合框架的好处 -简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。 +1. 容量自增长; +2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量; +3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。 +4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。 -**FAB 法则(Feature Advantage Benefit)** -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 +### 常用的集合类有哪些? -简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。 +* Map接口和Collection接口是所有集合框架的父接口: +1. Collection接口的子接口包括:Set接口和List接口 +2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 +3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 +4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等 +### ArrayList和HashMap默认大小? -### 项目经历怎么写 +在 java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段 -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: +```java +private static final int DEFAULT_CAPACITY = 10; -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 + //from HashMap.java JDK 7 +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` +### List,Set,Map三者的区别? +### List、Map、Set三个接口,存取元素时,各有什么特点? -### 专业技能怎么写 +### 集合框架底层数据结构 -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写(下面这部分内容摘自我的简历,大家可以根据自己的情况做一些修改和完善): +### poll()方法和remove()方法区别? -- 计算机网络、数据结构、算法、操作系统等课内基础知识:掌握 -- Java 基础知识:掌握 -- JVM 虚拟机(Java内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM内存管理):掌握 -- 高并发、高可用、高性能系统开发:掌握 -- Struts2、Spring、Hibernate、Ajax、Mybatis、JQuery :掌握 -- SSH 整合、SSM 整合、 SOA 架构:掌握 -- Dubbo: 掌握 -- Zookeeper: 掌握 -- 常见消息队列: 掌握 -- Linux:掌握 -- MySQL常见优化手段:掌握 -- Spring Boot +Spring Cloud +Docker:了解 -- Hadoop 生态相关技术中的 HDFS、Storm、MapReduce、Hive、Hbase :了解 -- Python 基础、一些常见第三方库比如OpenCV、wxpy、wordcloud、matplotlib:熟悉 +### ArrayList,Vector,LinkedList的存储性能和特性 +### ArrayList和Array有什么区别? +## 更多Java集合 & 容器面试题: [Java集合 & 容器 面试题](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/base/Java%E9%9B%86%E5%90%88&%E5%AE%B9%E5%99%A8.md) -### 排版注意事项 +完整PDF下载:[高清Java面试合集 PDF 下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/qr.md) -1. 尽量简洁,不要太花里胡哨; -2. 一些技术名词不要弄错了大小写比如MySQL不要写成mysql,Java不要写成java。这个在我看来还是比较忌讳的,所以一定要注意这个细节; -3. 中文和数字英文之间加上空格的话看起来会舒服一点; -### 其他一些小tips +Array可以容纳基本类型和对象,而ArrayList只能容纳对象。 -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -3. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -4. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -5. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -6. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -7. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 \ No newline at end of file +ArrayList 是Java集合框架类的一员,可以称它为一个动态数组. array 是静态的,所以一个数据一旦创建就无法更改他的大小 \ No newline at end of file diff --git a/interviewDoc/Java/base/JVM.md b/interviewDoc/Java/base/JVM.md new file mode 100644 index 0000000..6143e0b --- /dev/null +++ b/interviewDoc/Java/base/JVM.md @@ -0,0 +1,1076 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + + + +- [对象在哪块内存分配?](#对象在哪块内存分配) +- [谈谈 JVM 中的常量池](#谈谈-jvm-中的常量池) +- [谈谈动态年龄判断](#谈谈动态年龄判断) +- [谈谈永久代](#谈谈永久代) +- [JVM 有哪些运行时内存区域?](#jvm-有哪些运行时内存区域) +- [运行时栈帧包含哪些结构?](#运行时栈帧包含哪些结构) +- [JVM 如何确定垃圾对象?](#jvm-如何确定垃圾对象) +- [哪些是 GC Roots?](#哪些是-gc-roots) +- [如何开启和查看 GC 日志?](#如何开启和查看-gc-日志) +- [说一下垃圾分代收集的过程](#说一下垃圾分代收集的过程) +- [如何找到死锁的线程?](#如何找到死锁的线程) +- [invokedynamic 指令是干什么的?](#invokedynamic-指令是干什么的) +- [什么是逃逸分析?](#什么是逃逸分析) +- [什么是方法内联?](#什么是方法内联) +- [JVM 监控与分析工具你用过哪些?介绍一下。](#jvm-监控与分析工具你用过哪些介绍一下) +- [描述一下什么情况下,对象会从年轻代进入老年代](#描述一下什么情况下对象会从年轻代进入老年代) +- [工作中常用的 JVM 配置参数有哪些?](#工作中常用的-jvm-配置参数有哪些) +- [JIT 是什么?](#jit-是什么) +- [谈谈对 OOM 的认识](#谈谈对-oom-的认识) +- [你有哪些手段来排查 OOM 的问题?](#你有哪些手段来排查-oom-的问题) +- [什么情况发生栈溢出?](#什么情况发生栈溢出) +- [遇到过堆外内存溢出吗?](#遇到过堆外内存溢出吗) +- [遇到过元空间溢出吗?](#遇到过元空间溢出吗) +- [被引用的对象就一定能存活吗?](#被引用的对象就一定能存活吗) +- [你做过 JVM 调优,说说如何查看 JVM 参数默认值?](#你做过-jvm-调优说说如何查看-jvm-参数默认值) +- [什么是字节码?采用字节码的最大好处是什么](#什么是字节码采用字节码的最大好处是什么) +- [什么情况下会发生栈内存溢出。](#什么情况下会发生栈内存溢出) +- [详解JVM内存模型](#详解jvm内存模型) +- [JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。](#jvm内存为什么要分成新生代老年代持久代新生代中为什么要分为eden和survivor) +- [JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代](#jvm中一次完整的gc流程是怎样的对象如何晋升到老年代) +- [你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。](#你知道哪几种垃圾收集器各自的优缺点重点讲下cms和g1包括原理流程优缺点) +- [如何查看 JVM 当前使用的是什么垃圾收集器?](#如何查看-jvm-当前使用的是什么垃圾收集器) +- [JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。](#jvm内存模型的相关知识了解多少比如重排序内存屏障happen-before主内存工作内存) +- [谈谈你知道的垃圾回收算法](#谈谈你知道的垃圾回收算法) +- [谈谈你知道的垃圾收集器](#谈谈你知道的垃圾收集器) +- [生产环境用的什么JDK?如何配置的垃圾收集器?](#生产环境用的什么jdk如何配置的垃圾收集器) +- [谈谈双亲委派模型](#谈谈双亲委派模型) +- [简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。](#简单说说你了解的类加载器可以打破双亲委派么怎么打破) +- [列举一些你知道的打破双亲委派机制的例子。为什么要打破?](#列举一些你知道的打破双亲委派机制的例子为什么要打破) +- [怎么打出线程栈信息。](#怎么打出线程栈信息) +- [强引用、软引用、弱引用、虚引用的区别?](#强引用软引用弱引用虚引用的区别) +- [safepoint 是什么?](#safepoint-是什么) +- [MinorGC、MajorGC、FullGC 什么时候发生?](#minorgcmajorgcfullgc-什么时候发生) +- [说说类加载的过程](#说说类加载的过程) +- [可以描述一下 class 文件的结构吗?](#可以描述一下-class-文件的结构吗) +- [说说 JVM 如何执行 class 中的字节码。](#说说-jvm-如何执行-class-中的字节码) +- [生产环境 CPU 占用过高,你如何解决?](#生产环境-cpu-占用过高你如何解决) +- [生产环境服务器变慢,如何诊断处理?](#生产环境服务器变慢如何诊断处理) + + +### 对象在哪块内存分配? + +数组和对象在堆内存分配;某些对象没有逃逸出方法,可能被优化为在栈上分配 + + + +### 谈谈 JVM 中的常量池 + +**JDK 1.8 开始** + +字符串常量池:存放在堆中,包括 String 对象执行 intern() 方法后存的地方、双引号直接引用的字符串 + +运行时常量池:存放在方法区,属于元空间,是类加载后的一些存储区域,大多数是类中 constant_pool 的内容 + +类文件常量池:constant_pool,JVM 定义的概念 + + + +### 谈谈动态年龄判断 + +这里涉及到 `-XX:TargetSurvivorRatio` 参数,Survivor 区的目标使用率默认 50,即 Survivor 区对象目标使用率为 50%。 + + +Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。 + + +当然,这里还需要考虑参数 `-XX:MaxTenuringThreshold` 晋升年龄最大阈值 + + + +### 谈谈永久代 + +JDK 8 之前,Hotspot 中方法区的实现是永久代(Perm) + +JDK 7 开始把原本放在永久代的字符串常量池、静态变量等移出到堆,JDK 8 开始去除永久代,使用元空间(Metaspace),永久代剩余内容移至元空间,元空间直接在本地内存分配。 + + + +### JVM 有哪些运行时内存区域? + +**Java 8** + +- The pc Register,程序计数器 +- Java Virtual Machine Stacks,Java 虚拟机栈 +- Heap,堆 +- Method Area,方法区 +- Run-Time Constant Pool,运行时常量池 +- Native Method Stacks,本地方法栈 + + + +### 运行时栈帧包含哪些结构? + +- 局部变量表 +- 操作数栈 +- 动态连接 +- 返回地址 +- 附加信息 + + + +### JVM 如何确定垃圾对象? + +JVM 采用的是可达性分析算法,通过 GC Roots 来判定对象是否存活,从 GC Roots 向下追溯、搜索,会产生 Reference Chain。当一个对象不能和任何一个 GC Root 产生关系时,就判定为垃圾。 + + + +软引用和弱引用,也会影响对象的回收。内存不足时会回收软引用对象;GC 时会回收弱引用对象。 + + + +### 哪些是 GC Roots? + +- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 +- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 +- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 +- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 +- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 +- 所有被同步锁(synchronized关键字)持有的对象。 +- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI中注册的回调、本地代码缓存等。 + + + +### 如何开启和查看 GC 日志? + +常见的 GC 日志开启参数包括: +`-Xloggc:filename`,指定日志文件路径 +`-XX:+PrintGC`,打印 GC 基本信息 +`-XX:+PrintGCDetails`,打印 GC 详细信息 +`-XX:+PrintGCTimeStamps`,打印 GC 时间戳 +`-XX:+PrintGCDateStamps`,打印 GC 日期与时间 +`-XX:+PrintHeapAtGC`,打印 GC 前后的堆、方法区、元空间可用容量变化 +`-XX:+PrintTenuringDistribution`,打印熬过收集后剩余对象的年龄分布信息,有助于 MaxTenuringThreshold 参数调优设置 +`-XX:+PrintAdaptiveSizePolicy`,打印收集器自动设置堆空间各分代区域大小、收集目标等自动调节的相关信息 +`-XX:+PrintGCApplicationConcurrentTime`,打印 GC 过程中用户线程并发时间 +`-XX:+PrintGCApplicationStoppedTime`,打印 GC 过程中用户线程停顿时间 +`-XX:+HeapDumpOnOutOfMemoryError`,堆 oom 时自动 dump +`-XX:HeapDumpPath`,堆 oom 时 dump 文件路径 + + + +Java 9 JVM 日志模块进行了重构,参数格式发生变化,这个需要知道。 + + + +GC 日志输出的格式,会随着上面的参数不同而发生变化。关注各个分代的内存使用情况、垃圾回收次数、垃圾回收的原因、垃圾回收占用的时间、吞吐量、用户线程停顿时间。 + + + +借助工具可视化工具可以更方便的分析,在线工具 GCeasy;离线版可以使用 GCViewer。 + + + +如果现场环境不允许,可以使用 JDK 自带的 jstat 工具监控观察 GC 情况。 + + + +### 说一下垃圾分代收集的过程 + +分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。 +新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。 +当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下: + +- 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区; +- Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区; +- 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代 +- Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50% +- Survivor 区内存不足会发生担保分配 +- 超过指定大小的对象可以直接进入老年代 +- Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC +- FullGC,整个堆的垃圾收集,触发条件: + 1.每次晋升到老年代的对象平均大小>老年代剩余空间 + 2.MinorGC后存活的对象超过了老年代剩余空间 + 3.元空间不足 + 4.System.gc() 可能会引起 + 5.CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成 + 6.堆内存分配很大的对象 + + + +### 如何找到死锁的线程? + +死锁的线程可以使用 jstack 指令 dump 出 JVM 的线程信息。 +jstack -l \ > threads.txt +有时候需要dump出现异常,可以加上 -F 指令,强制导出 +jstack -F -l \ > threads.txt + +如果存在死锁,一般在文件最后会提示找到 deadlock 的数量与线程信息 + + + +### invokedynamic 指令是干什么的? + +Java 7 开始,新引入的字节码指令,可以实现一些动态类型语言的功能。Java 8 的 Lambda 表达式就是通过 invokedynamic 指令实现,使用方法句柄实现。 + + + +### 什么是逃逸分析? + +分析对象动态作用域 + +- 当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸; +- 被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸; +- 从不逃逸 + +如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化,如栈上分配、标量替换、同步消除。 + + + +### 什么是方法内联? + +为了减少方法调用的开销,可以把一些短小的方法,纳入到目标方法的调用范围之内,这样就少了一次方法调用,提升速度 + + + +### JVM 监控与分析工具你用过哪些?介绍一下。 + +`jps`,显示系统所有虚拟机进程信息的命令行工具 +`jstat`,监视分析虚拟机运行状态的命令行工具 +`jinfo`,查看和调整虚拟机参数的命令行工具 +`jmap`,生成虚拟机堆内存转储快照的命令行工具 +`jhat`,显示和分析虚拟机的转储快照文件的命令行工具 +`jstack`,生成虚拟机的线程快照的命令行工具 +`jcmd`,虚拟机诊断工具,JDK 7 提供 +`jhsdb`,基于服务性代理实现的进程外可视化调试工具,JDK 9 提供 +`JConsole`,基于JMX的可视化监视和管理工具 +`jvisualvm`,图形化虚拟机使用情况的分析工具 +`Java Mission Control`,监控和管理 Java 应用程序的工具 + + +`MAT`,Memory Analyzer Tool,虚拟机内存分析工具 +`vjtools`,唯品会的包含核心类库与问题分析工具 +`arthas`,阿里开源的 Java 诊断工具 +`greys`,JVM进程执行过程中的异常诊断工具 +`GCHisto`,GC 分析工具 +`GCViewer`,GC 日志文件分析工具 +`GCeasy`,在线版 GC 日志文件分析工具 +`JProfiler`,检查、监控、追踪 Java 性能的工具 +`BTrace`,基于动态字节码修改技术(Hotswap)实现的Java程序追踪与分析工具 + + + +下面可以重点体验下: +JDK 自带的命令行工具方便快捷,不是特别复杂的问题可以快速定位; + +阿里的 arthas 命令行也不错; + +可视化工具 MAT、JProfiler 比较强大。 + + + +### 描述一下什么情况下,对象会从年轻代进入老年代 + +- 对象的年龄超过一定阀值,-XX:MaxTenuringThreshold 可以指定该阀值 +- 动态对象年龄判定,有的垃圾回收算法,比如 G1,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法 +- 大小超出某个阀值的对象将直接在老年代上分配,值默认为 0,意思是全部首选 Eden 区进行分配,-XX:PretenureSizeThreshold 可以指定该阀值,部分收集器不支持 +- 分配担保,当 Survivor 空间不够的时候,则需要依赖其他内存(指老年代)进行分配担保,这个时候,对象也会直接在老年代上分配 + + + +### 工作中常用的 JVM 配置参数有哪些? + +Java 8 为例 + +**日志** +-XX:+PrintFlagsFinal,打印JVM所有参数的值 +-XX:+PrintGC,打印GC信息 +-XX:+PrintGCDetails,打印GC详细信息 +-XX:+PrintGCTimeStamps,打印GC的时间戳 +-Xloggc:filename,设置GC log文件的位置 +-XX:+PrintTenuringDistribution,查看熬过收集后剩余对象的年龄分布信息 + +**内存设置** +-Xms,设置堆的初始化内存大小 +-Xmx,设置堆的最大内存 +-Xmn,设置新生代内存大小 +-Xss,设置线程栈大小 +-XX:NewRatio,新生代与老年代比值 +-XX:SurvivorRatio,新生代中Eden区与两个Survivor区的比值,默认为8,即Eden:Survivor:Survivor=8:1:1 +-XX:MaxTenuringThreshold,从年轻代到老年代,最大晋升年龄。CMS 下默认为 6,G1 下默认为 15 +-XX:MetaspaceSize,设置元空间的大小,第一次超过将触发 GC +-XX:MaxMetaspaceSize,元空间最大值 +-XX:MaxDirectMemorySize,用于设置直接内存的最大值,限制通过 DirectByteBuffer 申请的内存 +-XX:ReservedCodeCacheSize,用于设置 JIT 编译后的代码存放区大小,如果观察到这个值有限制,可以适当调大,一般够用即可 + +**设置垃圾收集相关** +-XX:+UseSerialGC,设置串行收集器 +-XX:+UseParallelGC,设置并行收集器 +-XX:+UseConcMarkSweepGC,使用CMS收集器 +-XX:ParallelGCThreads,设置Parallel GC的线程数 +-XX:MaxGCPauseMillis,GC最大暂停时间 ms +-XX:+UseG1GC,使用G1垃圾收集器 + +CMS 垃圾回收器相关 +-XX:+UseCMSInitiatingOccupancyOnly +-XX:CMSInitiatingOccupancyFraction,与前者配合使用,指定MajorGC的发生时机 +-XX:+ExplicitGCInvokesConcurrent,代码调用 System.gc() 开始并行 FullGC,建议加上这个参数 +-XX:+CMSScavengeBeforeRemark,表示开启或关闭在 CMS 重新标记阶段之前的清除(YGC)尝试,它可以降低 remark 时间,建议加上 +-XX:+ParallelRefProcEnabled,可以用来并行处理 Reference,以加快处理速度,缩短耗时 + +**G1 垃圾回收器相关** +-XX:MaxGCPauseMillis,用于设置目标停顿时间,G1 会尽力达成 +-XX:G1HeapRegionSize,用于设置小堆区大小,建议保持默认 +-XX:InitiatingHeapOccupancyPercent,表示当整个堆内存使用达到一定比例(默认是 45%),并发标记阶段就会被启动 +-XX:ConcGCThreads,表示并发垃圾收集器使用的线程数量,默认值随 JVM 运行的平台不同而变动,不建议修改 + +参数查询官网地址: +https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html + + + +建议面试时最好能记住 CMS 和 G1的参数,特点突出使用较多,被问的概率大 + + + +### JIT 是什么? + +Just In Time Compiler 的简称,即时编译器。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器就是 JIT。 + + + +### 谈谈对 OOM 的认识 + +除了程序计数器,其他内存区域都有 OOM 的风险。 + +- 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM +- Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效 +- 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错 +- 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等 +- 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请 + + + +### 你有哪些手段来排查 OOM 的问题? + +- 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录 +- 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域 +- 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 + + + +### 什么情况发生栈溢出? + +-Xss可以设置线程栈的大小,当线程方法递归调用层次太深或者栈帧中的局部变量过多时,会出现栈溢出错误 java.lang.StackOverflowError + + + +### 遇到过堆外内存溢出吗? + +Unsafe 类申请内存、JNI 对内存进行操作、Netty 调用操作系统的 malloc 函数的直接内存,这些内存是不受 JVM 控制的,不加限制的使用,很容易发生溢出。这种情况有个显著特点,dump 的堆文件信息正常甚至很小。 +-XX:MaxDirectMemorySize 可以指定最大直接内存,但限制不住所有堆外内存的使用。 + + + +### 遇到过元空间溢出吗? + +元空间在本地内存上,默认是没有上限的,不加限制出了问题会影响整个服务器的,所以也是比较危险的。-XX:MaxMetaspaceSize 可以指定最大值。 + + +一般使用动态代理的框架会生成很多 Java 类,如果占用空间超出了我们的设定最大值,会发生元空间溢出。 + + + +### 被引用的对象就一定能存活吗? + +不一定,看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候,即 OOM 前会被回收,但如果没有在 Reference Chain 中的对象就一定会被回收。 + + + +### 你做过 JVM 调优,说说如何查看 JVM 参数默认值? + +- jps -v 可以查看 jvm 进程显示指定的参数 +- 使用 -XX:+PrintFlagsFinal 可以看到 JVM 所有参数的值 +- jinfo 可以实时查看和调整虚拟机各项参数 + + + +### 什么是字节码?采用字节码的最大好处是什么 + +* **字节码**:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。 + +* **采用字节码的好处**: + + Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。 + +* **先看下java中的编译器和解释器**: + + Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。 + + Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。 + + + + + +### 什么情况下会发生栈内存溢出。 + +**思路:** 描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo。 + +**我的答案:** + +* 栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型 +* 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。 +* 如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多) +* 参数 -Xss 去调整JVM栈的大小 + + + +### 详解JVM内存模型 + +**思路:** 给面试官画一下JVM内存模型图,并描述每个模块的定义,作用,以及可能会存在的问题,如栈溢出等。 + +**我的答案:** + +* JVM内存结构 + +![image-20210814121511202](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210814121511202.png) + +程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。 + +Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。 + +Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。 + +Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。 + +方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享 + + + +### JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。 + +**思路:** 先讲一下JAVA堆,新生代的划分,再谈谈它们之间的转化,相互之间一些参数的配置(如: –XX:NewRatio,–XX:SurvivorRatio等),再解释为什么要这样划分,最好加一点自己的理解。 + +**我的答案:** + +**1)共享内存区划分** + +* 共享内存区 = 持久带 + 堆 + +* 持久带 = 方法区 + 其他 + +* Java堆 = 老年代 + 新生代 + +* 新生代 = Eden + S0 + S1 + +**2)一些参数的配置** + +* 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。 +* 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定) +* Survivor区中的对象被复制次数为15(对应虚拟机参数 -XX:+MaxTenuringThreshold) + +**3)为什么要分为Eden和Survivor?为什么要设置两个Survivor区?** + +* 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。 +* Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。 +* 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生) + + + +### JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代 + +**思路:** 先描述一下Java堆内存划分,再解释Minor GC,Major GC,full GC,描述它们之间转化流程。 + +**我的答案:** + +* Java堆 = 老年代 + 新生代 +* 新生代 = Eden + S0 + S1 +* 当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。 +* **大对象**(需要大量连续内存空间的Java对象,如那种很长的字符串)**直接进入老年态**; +* 如果对象在Eden出生,并经过第一次Minor GC后仍然存活,并且被Survivor容纳的话,年龄设为1,每熬过一次Minor GC,年龄+1,**若年龄超过一定限制(15),则被晋升到老年态**。即**长期存活的对象进入老年态**。 +* 老年代满了而**无法容纳更多的对象** ,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – **包括年轻代和年老代**。 +* Major GC **发生在老年代的GC** ,**清理老年区**,经常会伴随至少一次Minor GC,**比Minor GC慢10倍以上**。 + + + +### 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。 + +**思路:** 一定要记住典型的垃圾收集器,尤其cms和G1,它们的原理与区别,涉及的垃圾回收算法。 + +**我的答案:** + +**1)几种垃圾收集器:** + +* **Serial收集器:** 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。 +* **ParNew收集器:** Serial收集器的多线程版本,也需要stop the world,复制算法。 +* **Parallel Scavenge收集器:** 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。 +* **Serial Old收集器:** 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。 +* **Parallel Old收集器:** 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。 +* **CMS(Concurrent Mark Sweep) 收集器:** 是一种以获得最短回收停顿时间为目标的收集器,**标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除**,收集结束会产生大量空间碎片。 +* **G1收集器:** 标记整理算法实现,**运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记**。不会产生空间碎片,可以精确地控制停顿。 + +**2)CMS收集器和G1收集器的区别:** + +* CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用; +* G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用; +* CMS收集器以最小的停顿时间为目标的收集器; +* G1收集器可预测垃圾回收的停顿时间 +* CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片 +* G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。 + + + +### 如何查看 JVM 当前使用的是什么垃圾收集器? + +-XX:+PrintCommandLineFlags 参数可以打印出所选垃圾收集器和堆空间大小等设置 + +如果开启了 GC 日志详细信息,里面也会包含各代使用的垃圾收集器的简称 + + + +### JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存。 + +**思路:** 先画出Java内存模型图,结合例子volatile ,说明什么是重排序,内存屏障,最好能给面试官写以下demo说明。 + +**我的答案:** + +**1)Java内存模型图:** + +![image-20210814121726796](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210814121726796.png) + +Java内存模型规定了所有的**变量都存储在主内存**中,每条**线程还有自己的工作内存**,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,**线程对变量的所有操作都必须在工作内存中**进行,**而不能直接读写主内存**。不同的线程之间也**无法直接访问对方工作内存中的变量**,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。 + +**2)指令重排序。** + +在这里,先看一段代码 + +``` +public class PossibleReordering { +static int x = 0, y = 0; +static int a = 0, b = 0; + +public static void main(String[] args) throws InterruptedException { + Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); + Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start();other.start(); one.join();other.join(); System.out.println(“(” + x + “,” + y + “)”);} + +``` + +运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是**指令重排**。 + +**3)内存屏障** + +**内存屏障**,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。 + +* **LoadLoad屏障** :对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 +* **StoreStore屏障** :对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 +* **LoadStore屏障** :对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 +* **StoreLoad屏障** :对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 + +**4)happen-before原则** + +* **单线程happen-before原则**:在同一个线程中,书写在前面的操作happen-before后面的操作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。 +* **volatile的happen-before原则**:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。 +* **happen-before的传递性原则**:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。 +* **线程启动的happen-before原则**:同一个线程的start方法happen-before此线程的其它方法。 +* **线程中断的happen-before原则** :对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。 +* **线程终结的happen-before原则:** 线程中的所有操作都happen-before线程的终止检测。 +* **对象创建的happen-before原则:** 一个对象的初始化完成先于他的finalize方法调用。 + + + +### 谈谈你知道的垃圾回收算法 + +判断对象是否可回收的算法有两种: + +- **Reference Counting GC,引用计数算法** +- **Tracing GC,可达性分析算法** + +JVM 各厂商基本都是用的 Tracing GC 实现 + +大部分垃圾收集器遵从了分代收集(Generational Collection)理论。 +针对新生代与老年代回收垃圾内存的特点,提出了 3 种不同的算法: + +**1、标记-清除算法(Mark-Sweep)** +标记需回收对象,统一回收;或标记存活对象,回收未标记对象。 +缺点: + +- 大量对象需要标记与清除时,效率不高 +- 标记、清除产生的大量不连续内存碎片,导致无法分配大对象 + + + +**2、标记-复制算法(Mark-Copy)** +可用内存等分两块,使用其中一块 A,用完将存活的对象复制到另外一块 B,一次性清空 A,然后改分配新对象到 B,如此循环。 +缺点: + +- 不适合大量对象不可回收的情况,换句话说就是仅适合大量对象可回收,少量对象需复制的区域 +- 只能使用内存容量的一半,浪费较多内存空间 + + + +**3、标记-整理算法(Mark-Compact)** +标记存活的对象,统一移到内存区域的一边,清空占用内存边界以外的内存。 +缺点: + +- 移动大量存活对象并更新引用,需暂停程序运行 + + + +### 谈谈你知道的垃圾收集器 + +**Serial** +特点: + +- JDK 1.3 开始提供 +- 新生代收集器 +- 无线程交互开销,单线程收集效率最高 +- 进行垃圾收集时需要暂停用户线程 +- 适用于客户端,小内存堆的回收 + + + +**ParNew** +特点: + +- 是 Serial 收集器的多线程并行版 +- JDK 7 之前首选的新生代收集器 +- 第一款支持并发的收集器,首次实现垃圾收集线程与用户线程基本上同时工作 +- 除 Serial 外,只有它能与 CMS 配合 + + + +**Parallel Scavenge** +特点: + +- 新生代收集器 +- 标记-复制算法 +- 多线程并行收集器 +- 追求高吞吐量,即最小的垃圾收集时间 +- 可以配置最大停顿时间、垃圾收集时间占比 +- 支持开启垃圾收集自适应调节策略,追求适合的停顿时间或最大的吞吐量 + + + +**Serial Old** +特点: + +- 与 Serial 类似,是 Serial 收集器的老年代版本 +- 使用标记-整理算法 + + + +**Parallel Old** +特点: + +- JDK 6 开始提供 +- Parallel Scavenge 的老年代版 +- 支持多线程并发收集 +- 标记-整理算法 +- Parallel Scavenge + Parallel Old 是一个追求高吞吐量的组合 + + + +**CMS** +特点: + +- 标记-清除算法 +- 追求最短回收停顿时间 +- 多应用于关注响应时间的 B/S 架构的服务端 +- 并发收集、低停顿 +- 占用一部分线程资源,应用程序变慢,吞吐量下降 +- 无法处理浮动垃圾,可能导致 Full GC +- 内存碎片化问题 + + + +**G1** +特点: + +- JDK 6 开始实验,JDK 7 商用 +- 面向服务端,JDK 9 取代 Parallel Scavenge + Parallel Old +- 结合标记-整理、标记-复制算法 +- 首创局部内存回收设计思路 +- 基于 Region 内存布局,采用不同策略实现分代 +- 不再使用固定大小、固定数量的堆内存分代区域划分 +- 优先回收价收益最大的 Region +- 单个或多个 Humongous 区域存放大对象 +- 使用记忆集解决跨 Region 引用问题 +- 复杂的卡表实现,导致更高的内存占用,堆的 10%~20% +- 全功能垃圾收集器 +- 追求有限的时间内最高收集效率、延迟可控的情况下最高吞吐量 +- 追求应付内存分配速率,而非一次性清掉所有垃圾内存 +- 适用于大内存堆 + + + +**Shenandoah** +特点: + +- 追求低延迟,停顿 10 毫秒以内 +- OpenJDK 12 新特性,RedHat 提供 +- 连接矩阵代替记忆集,降低内存使用与伪共享问题出现概率 + + + +**ZGC** +特点: + +- JDK 11 新加的实验性质的收集器 +- 追求低延迟,停顿 10 毫秒以内 +- 基于 Region 内存布局 +- 未设分代 +- 读屏障、染色指针、内存多重映射实现可并发的标记-整理算法 +- 染色指针和内存多重映射设计精巧,解决部分性能问题,但降低了可用最大内存、操作系统受限、只支持 32 位、不支持压缩指针等 +- 成绩亮眼、性能彪悍 + + + +### 生产环境用的什么JDK?如何配置的垃圾收集器? + +Oracle JDK 1.8 + +JDK 1.8 中有 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,默认使用 Parallel Scavenge + Parallel Old。 + +- Serial 系列是单线程垃圾收集器,处理效率很高,适合小内存、客户端场景使用,使用参数 -XX:+UseSerialGC 显式启用。 + +- Parallel 系列相当于并发版的 Serial,追求高吞吐量,适用于较大内存并且有多核CPU的环境,默认或显式使用参数 -XX:+UseParallelGC 启用。可以使用 -XX:MaxGCPauseMillis 参数指定最大垃圾收集暂停毫秒数,收集器会尽量达到目标;使用 -XX:GCTimeRatio 指定期望吞吐量大小,默认 99,用户代码运行时间:垃圾收集时间=99:1。 + +- CMS,追求垃圾收集暂停时间尽可能短,适用于服务端较大内存且多 CPU 的应用,使用参数 -XX:+UseConcMarkSweepGC 显式开启,会同时作用年轻代与老年代,但有浮动垃圾和内存碎片化的问题。 + +- G1,主要面向服务端应用的垃圾收集器,适用于具有大内存的多核 CPU 的服务器,追求较小的垃圾收集暂停时间和较高的吞吐量。首创局部内存回收设计思路,采用不同策略实现分代,不再使用固定大小、固定数量的堆内存分代区域划分,而是基于 Region 内存布局,优先回收价收益最大的 Region。使用参数 -XX:+UseG1GC 开启。 + + 我们生产环境使用了 G1 收集器,相关配置如下 + -Xmx12g + -Xms12g + -XX:+UseG1GC + -XX:InitiatingHeapOccupancyPercent=45 + -XX:MaxGCPauseMillis=200 + -XX:MetaspaceSize=256m + -XX:MaxMetaspaceSize=256m + -XX:MaxDirectMemorySize=512m + + -XX:G1HeapRegionSize 未指定 + + **核心思路:** + 每个内存区域设置上限,避免溢出 + + 堆设置为操作系统的 70%左右,超过 8 G,首选 G1 + + 根据老年代对象提升速度,调整新生代与老年代之间的内存比例 + 等过 GC 信息,针对项目敏感指标优化,比如访问延迟、吞吐量等 + + + +### 谈谈双亲委派模型 + +Parents Delegation Model,这里的 Parents 翻译成双亲有点不妥,类加载向上传递的过程中只有单亲;parents 更多的是多级向上的意思。 + +除了顶层的启动类加载器,其他的类加载器在加载之前,都会委派给它的父加载器进行加载,一层层向上传递,直到所有父类加载器都无法加载,自己才会加载该类。 + +双亲委派模型,更好地解决了各个类加载器协作时基础类的一致性问题,避免类的重复加载;防止核心API库被随意篡改。 + + +JDK 9 之前 + +- 启动类加载器(Bootstrp ClassLoader),加载 /lib/rt.jar、-Xbootclasspath +- 扩展类加载器(Extension ClassLoader)sun.misc.Launcher$ExtClassLoader,加载 /lib/ext、java.ext.dirs +- 应用程序类加载器(Application ClassLoader,sun.misc.Launcher$AppClassLoader),加载 CLASSPTH、-classpath、-cp、Manifest +- 自定义类加载器 + +JDK 9 开始 Extension ClassLoader 被 Platform ClassLoader 取代,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader + +类加载代码逻辑 + +```java +protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // 首先,检查请求的类是否已经被加载过了 + Class c = findLoadedClass(name); + if (c == null) { + try { + if (parent != null) { + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + // 如果父类加载器抛出ClassNotFoundException + // 说明父类加载器无法完成加载请求 + } + if (c == null) { + // 在父类加载器无法加载时 + // 再调用本身的findClass方法来进行类加载 + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; +} +``` + + + +### 简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。 + +**思路:** 先说明一下什么是类加载器,可以给面试官画个图,再说一下类加载器存在的意义,说一下双亲委派模型,最后阐述怎么打破双亲委派模型。 + +**我的答案:** + +**1) 什么是类加载器?** + +**类加载器** 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。 + +> * 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。 +> * 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如: +> +> > * 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。 +> > * 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 + +**2)双亲委派模型** + +**双亲委派模型工作过程是:** + +> 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。 + +双亲委派模型图: + +![image-20210814122654238](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210814122654238.png) + +**3)为什么需要双亲委派模型?** + +在这里,先想一下,如果没有双亲委派,那么用户是不是可以**自己定义一个java.lang.Object的同名类**,**java.lang.String的同名类**,并把它放到ClassPath中,那么**类之间的比较结果及类的唯一性将无法保证**,因此,为什么需要双亲委派模型?**防止内存中出现多份同样的字节码** + +**4)怎么打破双亲委派模型?** + +打破双亲委派机制则不仅**要继承ClassLoader**类,还要**重写loadClass和findClass**方法。 + + + +### 列举一些你知道的打破双亲委派机制的例子。为什么要打破? + +- JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。 +- Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。 +- OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。 +- JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。 + + + +### 怎么打出线程栈信息。 + +**思路:** 可以说一下jps,top ,jstack这几个命令,再配合一次排查线上问题进行解答。 + +**我的答案:** + +* 输入jps,获得进程号。 +* top -Hp pid 获取本进程中所有线程的CPU耗时性能 +* jstack pid命令查看当前java进程的堆栈状态 +* 或者 jstack -l > /tmp/output.txt 把堆栈信息打到一个txt文件。 +* 可以使用fastthread 堆栈定位,[fastthread.io/](http://fastthread.io/) + + + +### 强引用、软引用、弱引用、虚引用的区别? + +**思路:** 先说一下四种引用的定义,可以结合代码讲一下,也可以扩展谈到ThreadLocalMap里弱引用用处。 + +**我的答案:** + +**1)强引用** + +我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。 + +**2)软引用** + +如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。 + +``` +SoftReference softRef=new SoftReference(str); // 软引用 + +``` + +**用处:** 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。 + +(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建 + +(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出 + +如下代码: + +``` +Browser prev = new Browser(); // 获取页面进行浏览 +SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用 +if(sr.get()!=null){ +rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取 +}else{ + prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了 + sr = new SoftReference(prev); // 重新构建 +} + +``` + +**3)弱引用** + +具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 + +``` +String str=new String("abc"); +WeakReference abcWeakRef = new WeakReference(str); +str=null; +等价于 +str = null; +System.gc(); + +``` + +**4)虚引用** + +如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。 + + + +### safepoint 是什么? + +为了减少对象引用的扫描,使用 OopMap 的数据结构在特定的位置记录下栈里和寄存器里哪些位置是引用; +但为了避免给每条指令都生成 OopMap 记录占用大量内存的问题,只在特定位置记录这些信息。 +安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,如方法调用、循环跳转、异常跳转等都属于指令序列复用。 + + + +### MinorGC、MajorGC、FullGC 什么时候发生? + +- MinorGC 在年轻代空间不足的时候发生 +- MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC +- FullGC 老年代无法再分配内存;元空间不足;显示调用 System.gc;像 CMS 一类的垃圾回收器,在 MinorGC 出现 promotion failure 时也会发生 FullGC + + + +### 说说类加载的过程 + +- 加载(Loading),通过一个类的全限定名来获取定义此类的二进制字节流;将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 +- 验证(Verification),确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 +- 准备(Preparation),正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值。 +- 解析(Resolution),是 JVM 将常量池内的符号引用替换为直接引用的过程。 +- 初始化(Initialization),执行类构造器 方法的过程,执行所有类变量的赋值动作和静态语句块(static{}块)。 + +其中验证、准备、解析统称为称为连接(Linking) + + + +### 可以描述一下 class 文件的结构吗? + +Class 文件包含了 Java 虚拟机的指令集、符号表、辅助信息的字节码(Byte Code),是实现跨操作系统和语言无关性的基石之一。 + + +一个 Class 文件定义了一个类或接口的信息,是以 8 个字节为单位,没有分隔符,按顺序紧凑排在一起的二进制流。 + + +用 "无符号数" 和 "表" 组成的伪结构来存储数据。 + +- 无符号数:基本数据类型,用来描述数字、索引引用、数量值、字符串值,如u1、u2 分别表示 1 个字节、2 个字节 +- 表:无符号数和其他表组成,命名一般以 "_info" 结尾 + + + +组成部分 +**1、魔数 Magic Number** + +- Class 文件头 4 个字节,0xCAFEBABE +- 作用是确定该文件是 Class 文件 + +**2、版本号** + +- 4 个字节,前 2 个是次版本号 Minor Version,后 2 个主版本号 Major Version +- 从 45 (JDK1.0) 开始,如 0x00000032 转十进制就是 50,代表 JDK 6 +- 低版本的虚拟机跑不了高版本的 Class 文件 + +**3、常量池** + +- 常量容量计数值(constant_pool_count),u2,从 1 开始。如 0x0016 十进制 22 代表有 21 项常量 +- 每项常量都是一个表,目前 17 种 +- 特点:Class 文件中最大数据项目之一、第一个出现表数据结构 + +**4、访问标志** + +- 2 个字节,表示类或接口的访问标志 + +**5、类索引、父类索引、接口索引集合** + +- 类索引(this_class)、父类索引(super_class),u2 +- 接口索引集合(interfaces),u2 集合 +- 类索引确定类的全限定名、父类索引确定父类的全限定名、接口索引集合确定实现接口 +- 索引值在常量池中查找对应的常量 + +**6、字段表(field_info)集合** + +- 描述接口或类申明的变量 +- fields_count,u2,表示字段表数量;后面接着相应数量的字段表 +- 9 种字段访问标志 + +**7、方法表(method_info)集合** + +- 描述接口或类申明的方法 +- methods_count,u2,表示方法表数量;后面接着相应数量的方法表 +- 12 种方法访问标志 +- 方法表结构与字段表结构一致 + +**8、属性表(attribute_info)集合** + +- class 文件、字段表、方法表可携带属性集合,描述特有信息 +- 预定义 29 项属性,可自定义写入不重名属性 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/YQyH9z56c4T5HvkVxibC1VFSDrpxoGicsbgAFvZC8Cn5g9duaZ6pSrPDffOicNGm4OLecsqveEwkPXdRiaCPqgR5Kg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +### 说说 JVM 如何执行 class 中的字节码。 + +JVM 先加载包含字节码的 class 文件,存放在方法区,实际运行时,虚拟机会执行方法区内的代码。Java 虚拟机在内存中划分出栈和堆来存储运行时的数据。 + + +运行过程中,每当调用进入 Java 方法,都会在 Java 方法栈中生成一个栈帧,用来支持虚拟机进行方法的调用与执行,包含了局部变量表、操作数栈、动态链接、方法返回地址等信息。 + + +当退出当前执行的方法时,不管正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。 + + +方法的调用,需要通过解析完成符号引用到直接引用;通过分派完成动态找到被调用的方法。 + + +从硬件角度来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。翻译过程由两种形式:第一种是解释执行,即将遇到的字节一边码翻译成机器码一边执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。在 HotSpot 里两者都有,解释执行在启动时节约编译时间执行速度较快;随着时间的推移,编译器逐渐会返回作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。 + + + +### 生产环境 CPU 占用过高,你如何解决? + +**1、** top + H 指令找出占用 CPU 最高的进程的 pid + +**2、** top -H -p + +在该进程中找到,哪些线程占用的 CPU 最高的线程,记录下 tid + +**3、** jstack -l + +\> threads.txt,导出进程的线程栈信息到文本,导出出现异常的话,加上 -F 参数 + +**4、** 将 tid 转换为十六进制,在 threads.txt 中搜索,查到对应的线程代码执行栈,在代码中查找占 CPU 比较高的原因。其中 tid 转十六进制,可以借助 Linux 的 printf "%x" tid 指令 + + + +我用上述方法查到过,jvm 多条线程疯狂 full gc 导致的CPU 100% 的问题和 JDK1.6 HashMap 并发 put 导致线程 CPU 100% 的问题 + + + +### 生产环境服务器变慢,如何诊断处理? + +使用 top 指令,服务器中 CPU 和 内存的使用情况,-H 可以按 CPU 使用率降序,-M 内存使用率降序。排除其他进程占用过高的硬件资源,对 Java 服务造成影响。 + + +如果发现 CPU 使用过高,可以使用 top 指令查出 JVM 中占用 CPU 过高的线程,通过 jstack 找到对应的线程代码调用,排查出问题代码。 + + +如果发现内存使用率比较高,可以 dump 出 JVM 堆内存,然后借助 MAT 进行分析,查出大对象或者占用最多的对象来自哪里,为什么会长时间占用这么多;如果 dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令 pmap 查出进程的内存分配情况、gdb dump 出具体内存信息、perf 查看本地函数调用等。 + + + +如果 CPU 和 内存使用率都很正常,那就需要进一步开启 GC 日志,分析用户线程暂停的时间、各部分内存区域 GC 次数和时间等指标,可以借助 jstat 或可视化工具 GCeasy 等,如果问题出在 GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;分析 jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启 jmx,使用 visualmv 等可视化工具远程监控与分析。 \ No newline at end of file diff --git "a/interviewDoc/Java/base/Java\345\237\272\347\241\200.md" "b/interviewDoc/Java/base/Java\345\237\272\347\241\200.md" new file mode 100644 index 0000000..8691083 --- /dev/null +++ "b/interviewDoc/Java/base/Java\345\237\272\347\241\200.md" @@ -0,0 +1,2722 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + + + +- [基本概念](#基本概念) + - [什么是Java](#什么是java) + - [Java SE 、Java EE 、Java ME](#java-se-java-ee-java-me) + - [Java的特点有哪些](#java的特点有哪些) + - [JDK和JRE和JVM的区别](#jdk和jre和jvm的区别) + - [什么是跨平台性?原理是什么](#什么是跨平台性原理是什么) + - [Oracle JDK 和 OpenJDK 的对比](#oracle-jdk-和-openjdk-的对比) + - [Java和C++的区别](#java和c的区别) + - [什么Java注释](#什么java注释) + - [Java程序是如何执行的](#java程序是如何执行的) +- [基础部分](#基础部分) + - [instanceof 关键字的作用](#instanceof-关键字的作用) + - [== 和 equals 的区别是什么?](#-和-equals-的区别是什么) + - [什么是java序列化,如何实现java序列化?或者请解释Serializable接口的作用。](#什么是java序列化如何实现java序列化或者请解释serializable接口的作用) + - [Hashcode的作用](#hashcode的作用) + - [两个对象的 hashCode() 相同, 那么 equals() 也一定为 true吗?](#两个对象的-hashcode-相同-那么-equals-也一定为-true吗) + - [泛型常用特点](#泛型常用特点) + - [使用泛型的好处?](#使用泛型的好处) +- [数据类型](#数据类型) + - [Java有哪些数据类型](#java有哪些数据类型) + - [Java中引用数据类型有哪些,它们与基本数据类型有什么区别?](#java中引用数据类型有哪些它们与基本数据类型有什么区别) + - [Java的四种引用,强弱软虚](#java的四种引用强弱软虚) + - [自动装箱与拆箱](#自动装箱与拆箱) + - [int 和 Integer 有什么区别](#int-和-integer-有什么区别) + - [Integer a= 127 与 Integer b = 127相等吗](#integer-a-127-与-integer-b--127相等吗) + - [Java中的自动装箱与拆箱](#java中的自动装箱与拆箱) + - [为什么要有包装类型?](#为什么要有包装类型) + - [a=a+b与a+=b有什么区别吗?](#aab与ab有什么区别吗) + - [float f=3.4;是否正确](#float-f34是否正确) + - [short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗](#short-s1--1-s1--s1--1有错吗short-s1--1-s1--1有错吗) + - [能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象?](#能将-int-强制转换为-byte-类型的变量吗如果该值大于-byte-类型的范围将会出现什么现象) +- [常用关键字](#常用关键字) + - [final 在 Java 中有什么作用?](#final-在-java-中有什么作用) + - [访问修饰符 public,private,protected,以及不写(默认)时的区别](#访问修饰符-publicprivateprotected以及不写默认时的区别) + - [final有哪些用法?](#final有哪些用法) + - [final 有什么用?](#final-有什么用) + - [static都有哪些用法?](#static都有哪些用法) + - [static存在的主要意义](#static存在的主要意义) + - [static的独特之处](#static的独特之处) + - [static注意事项](#static注意事项) + - [static和final区别](#static和final区别) + - [this关键字的用法](#this关键字的用法) + - [super关键字的用法](#super关键字的用法) + - [&和&&的区别](#和的区别) + - [break ,continue ,return 的区别及作用](#break-continue-return-的区别及作用) +- [异常处理](#异常处理) + - [Java异常简介](#java异常简介) + - [Java异常架构](#java异常架构) + - [Error 和 Exception 有什么区别?](#error-和-exception-有什么区别) + - [阐述 final、finally、finalize 的区别](#阐述-finalfinallyfinalize-的区别) + - [列出一些你常见的运行时异常?](#列出一些你常见的运行时异常) + - [什么是受检异常](#什么是受检异常) + - [Excption与Error包结构](#excption与error包结构) + - [try {}里有一个return语句,那么紧跟在这个try后的finally{}里的code会不会被执行,什么时候被执行,在return前还是后?](#try-里有一个return语句那么紧跟在这个try后的finally里的code会不会被执行什么时候被执行在return前还是后) + - [运行时异常与一般异常有何异同?](#运行时异常与一般异常有何异同) + - [error和exception有什么区别?](#error和exception有什么区别) + - [简单说说Java中的异常处理机制的简单原理和应用。](#简单说说java中的异常处理机制的简单原理和应用) +- [面向对象](#面向对象) + - [面向对象和面向过程的区别](#面向对象和面向过程的区别) + - [面向对象的特征有哪些方面](#面向对象的特征有哪些方面) + - [Java多态的理解](#java多态的理解) + - [什么是多态机制?Java语言是如何实现多态的?](#什么是多态机制java语言是如何实现多态的) + - [面向对象五大基本原则是什么(可选)](#面向对象五大基本原则是什么可选) + - [抽象类和接口的对比](#抽象类和接口的对比) + - [普通类和抽象类有哪些区别?](#普通类和抽象类有哪些区别) + - [抽象类能使用 final 修饰吗?](#抽象类能使用-final-修饰吗) + - [创建一个对象用什么关键字?对象实例与对象引用有何不同?](#创建一个对象用什么关键字对象实例与对象引用有何不同) + - [构造器(constructor)是否可被重写(override)](#构造器constructor是否可被重写override) + - [重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?](#重载overload和重写override的区别重载的方法能否根据返回类型进行区分) +- [类、变量、方法](#类变量方法) + - [什么是内部类?](#什么是内部类) + - [内部类的分类有哪些](#内部类的分类有哪些) + - [静态内部类](#静态内部类) + - [成员内部类](#成员内部类) + - [局部内部类](#局部内部类) + - [匿名内部类](#匿名内部类) + - [内部类的优点](#内部类的优点) + - [内部类有哪些应用场景](#内部类有哪些应用场景) + - [局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?](#局部内部类和匿名内部类访问局部变量的时候为什么变量必须要加上final) + - [内部类相关,看程序说出运行结果](#内部类相关看程序说出运行结果) + - [成员变量与局部变量的区别有哪些](#成员变量与局部变量的区别有哪些) + - [在Java中定义一个不做事且没有参数的构造方法的作用](#在java中定义一个不做事且没有参数的构造方法的作用) + - [在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) + - [一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?](#一个类的构造方法的作用是什么若一个类没有声明构造方法改程序能正确执行吗为什么) + - [构造方法有哪些特性?](#构造方法有哪些特性) + - [静态变量和实例变量区别](#静态变量和实例变量区别) + - [静态变量与普通变量区别](#静态变量与普通变量区别) + - [静态方法和实例方法有何不同?](#静态方法和实例方法有何不同) + - [在一个静态方法内调用一个非静态成员为什么是非法的?](#在一个静态方法内调用一个非静态成员为什么是非法的) + - [什么是方法的返回值?返回值的作用是什么?](#什么是方法的返回值返回值的作用是什么) + - [什么是内部类?](#什么是内部类) + - [内部类的分类有哪些](#内部类的分类有哪些) + - [静态内部类](#静态内部类) + - [成员内部类](#成员内部类) + - [局部内部类](#局部内部类) + - [匿名内部类](#匿名内部类) + - [内部类的优点](#内部类的优点) + - [内部类有哪些应用场景](#内部类有哪些应用场景) + - [局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?](#局部内部类和匿名内部类访问局部变量的时候为什么变量必须要加上final) + - [内部类相关,看程序说出运行结果](#内部类相关看程序说出运行结果) +- [文件、I/O流](#文件io流) + - [Java 中有几种类型的流?](#java-中有几种类型的流) + - [写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。](#写一个方法输入一个文件名和一个字符串统计这个字符串在这个文件中出现的次数) + - [Java 中怎么创建 ByteBuffer?](#java-中怎么创建-bytebuffer) + - [说出 5 条 IO 的最佳实践(答案)](#说出-5-条-io-的最佳实践答案) + - [Java 中 IO 流分为几种?](#java-中-io-流分为几种) + - [谈谈Java IO里面的常见类,字节流,字符流、接口、实现类、方法阻塞](#谈谈java-io里面的常见类字节流字符流接口实现类方法阻塞) + - [字符流和字节流有什么区别?](#字符流和字节流有什么区别) + - [字节流和字符流,你更喜欢哪一个?](#字节流和字符流你更喜欢哪一个) + - [System.out.println()是什么?](#systemoutprintln是什么) + - [什么是Filter流?](#什么是filter流) + - [有哪些可用的Filter流?](#有哪些可用的filter流) + - [有哪些Filter流的子类?](#有哪些filter流的子类) + - [在**文件拷贝**的时候,哪一种流可用于提升更多的性能?](#在文件拷贝的时候哪一种流可用于提升更多的性能) + - [Java中流类的**超类**(**均为抽象类**)主要由哪些组成?](#java中流类的超类均为抽象类主要由哪些组成) + - [FileInputStream和FileOutputStream是什么?](#fileinputstream和fileoutputstream是什么) + - [BIO、NIO、AIO 有什么区别?](#bionioaio-有什么区别) + - [讲讲NIO](#讲讲nio) + - [Files的常用方法都有哪些?](#files的常用方法都有哪些) + - [Java 中 IO 流分为几种?](#java-中-io-流分为几种) + - [常见的NIO框架有哪些](#常见的nio框架有哪些) + - [Java IO 中的设计模式?(重点)](#java-io-中的设计模式重点) + - [在文件拷贝的时候,哪一种流可用于提升更多的性能?](#在文件拷贝的时候哪一种流可用于提升更多的性能) + - [说说管道流(Piped Stream)](#说说管道流piped-stream) + - [说说File类](#说说file类) +- [反射](#反射) + - [什么是反射机制?](#什么是反射机制) + - [反射机制优缺点](#反射机制优缺点) + - [反射机制的应用场景有哪些?](#反射机制的应用场景有哪些) + - [Java获取反射的三种方法](#java获取反射的三种方法) +- [字符串](#字符串) + - [字符型常量和字符串常量的区别](#字符型常量和字符串常量的区别) + - [什么是字符串常量池?](#什么是字符串常量池) + - [String 是最基本的数据类型吗](#string-是最基本的数据类型吗) + - [String有哪些特性](#string有哪些特性) + - [String为什么是不可变的吗?](#string为什么是不可变的吗) + - [String真的是不可变的吗?](#string真的是不可变的吗) + - [是否可以继承 String 类](#是否可以继承-string-类) + - [String str="i"与 String str=new String(“i”)一样吗?](#string-stri与-string-strnew-stringi一样吗) + - [String s = new String(“xyz”);创建了几个字符串对象](#string-s--new-stringxyz创建了几个字符串对象) + - [如何将字符串反转?](#如何将字符串反转) + - [数组有没有 length()方法?String 有没有 length()方法](#数组有没有-length方法string-有没有-length方法) + - [String 类的常用方法都有那些?](#string-类的常用方法都有那些) + - [在使用 HashMap 的时候,用 String 做 key 有什么好处?](#在使用-hashmap-的时候用-string-做-key-有什么好处) + - [String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的](#string和stringbufferstringbuilder的区别是什么string为什么是不可变的) + + + +## 基本概念 + +### 什么是Java + +Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 + + + +### Java SE 、Java EE 、Java ME + +* Java SE(J2SE,Java 2 Platform Standard Edition,标准版) + Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。 +* Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版) + Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE +* Java ME(J2ME,Java 2 Platform Micro Edition,微型版) + Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。 + + + +### Java的特点有哪些 + +Java 语言是一种分布式的面向对象语言,具有面向对象、平台无关性、简单性、解释执行、多线程、安全性等很多特点,下面针对这些特点进行逐一介绍。 + +**1. 面向对象** + +Java 是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包等均有很好的支持。为了简单起见,Java 只支持类之间的单继承,但是可以使用接口来实现多继承。使用 Java 语言开发程序,需要采用面向对象的思想设计程序和编写代码。 + +**2. 平台无关性** + +平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。 + +Java 语言使用 Java 虚拟机机制屏蔽了具体平台的相关信息,使得 Java 语言编译的程序只需生成虚拟机上的目标代码,就可以在多种平台上不加修改地运行。 + +**3. 简单性** + +Java 语言的语法与 C 语言和 C++ 语言很相近,使得很多程序员学起来很容易。对 Java 来说,它舍弃了很多 C++ 中难以理解的特性,如操作符的重载和多继承等,而且 Java 语言不使用指针,加入了垃圾回收机制,解决了程序员需要管理内存的问题,使编程变得更加简单。 + +**4. 解释执行** + +Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。在运行文件时,Java 的解释器对这些字节码进行解释执行,执行过程中需要加入的类在连接阶段被载入到运行环境中。 + +**5. 多线程** + +Java 语言是多线程的,这也是 Java 语言的一大特性,它必须由 Thread 类和它的子类来创建。Java 支持多个线程同时执行,并提供多线程之间的同步机制。任何一个线程都有自己的 run() 方法,要执行的方法就写在 run() 方法体内。 + +**6. 分布式** + +Java 语言支持 Internet 应用的开发,在 Java 的基本应用编程接口中就有一个网络应用编程接口,它提供了网络应用编程的类库,包括 URL、URLConnection、Socket 等。Java 的 RIM 机制也是开发分布式应用的重要手段。 + +**7. 健壮性** + +Java 的强类型机制、异常处理、垃圾回收机制等都是 Java 健壮性的重要保证。对指针的丢弃是 Java 的一大进步。另外,Java 的异常机制也是健壮性的一大体现。 + +**8. 高性能** + +Java 的高性能主要是相对其他高级脚本语言来说的,随着 JIT(Just in Time)的发展,Java 的运行速度也越来越高。 + +**9. 安全性** + +Java 通常被用在网络环境中,为此,Java 提供了一个安全机制以防止恶意代码的攻击。除了 Java 语言具有许多的安全特性以外,Java 还对通过网络下载的类增加一个安全防范机制,分配不同的名字空间以防替代本地的同名类,并包含安全管理机制。 + +Java 语言的众多特性使其在众多的编程语言中占有较大的市场份额,Java 语言对对象的支持和强大的 API 使得编程工作变得更加容易和快捷,大大降低了程序的开发成本。Java 的“一次编写,到处执行”正是它吸引众多商家和编程人员的一大优势。 + + + +### JDK和JRE和JVM的区别 + +**1. JDK** + +JDK(Java SE Development Kit),**Java标准的开发包,提供了编译、运行Java程序所需要的各种工具和资源**,包括了Java编译器、Java运行时环境、以及常用的Java类库等。 + + + +**2. JRE** + +JRE(Java Runtime Environment)**,Java运行时环境,用于解释执行Java的字节码文件**。普通用户只需要安装JRE来运行Java程序即可,而作为一名程序员必须安装JDK,来编译、调试程序。 + + + +**3. JVM** + +JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。**它是整个Java实现跨平台的核心**,负责解释执行字节码文件,是可运行Java字节码文件的虚拟计算机。所有平台上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。 + +当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。也就是说**JVM是运行Java字节码的虚拟机。** + +不同平台的JVM是不同的,但是他们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相同的虚拟机,编译后的Java字节码就可以在该平台上运行。 + + + +**为什么要采用字节码:** + +> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即Java源代码经过虚拟机编译器编译后扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 + + + +**什么是跨平台:** + +> 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 +> +> 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 + + + +**Java 程序从源代码到运行需要三步:** + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210321015133.png) + +**4. 总结** + +1. JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无序安装JDK。 +2. JDk包含JRE,JDK 和 JRE 中都包含 JVM。 +3. JVM 是 Java 编程语言的核心并且具有平台独立性。 + + + +### 什么是跨平台性?原理是什么 + +* 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 + +* 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 + + + +### Oracle JDK 和 OpenJDK 的对比 + +- Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次; +- OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的; +- Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题; +- 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能; +- Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; +- Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 + + + +### Java和C++的区别 + +我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来! + +* 都是面向对象的语言,都支持封装、继承和多态 +* Java不提供指针来直接访问内存,程序内存更加安全 +* Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。 +* Java有自动内存管理机制,不需要程序员手动释放无用内存 + + + +### 什么Java注释 + +**定义**:用于解释说明程序的文字 + +**分类** + +* 单行注释 + 格式: // 注释文字 +* 多行注释 + 格式: /* 注释文字 */ +* 文档注释 + 格式:/** 注释文字 */ + +**作用** + +* 在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。 + +**注意事项:** 多行和文档注释都不能嵌套使用。 + + + +### Java程序是如何执行的 + +我们日常的工作中都使用开发工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的调试程序,或者是通过打包工具把项目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常运行了,但你有没有想过 Java 程序内部是如何执行的?其实不论是在开发工具中运行还是在 Tomcat 中运行,Java 程序的执行流程基本都是相同的,它的执行流程如下: + +- 先把 Java 代码编译成字节码,也就是把 .java 类型的文件编译成 .class 类型的文件。这个过程的大致执行流程:Java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字符码生成器 -> 最终生成字节码,其中任何一个节点执行失败就会造成编译失败; +- 把 class 文件放置到 Java 虚拟机,这个虚拟机通常指的是 Oracle 官方自带的 Hotspot JVM; +- Java 虚拟机使用类加载器(Class Loader)装载 class 文件; +- 类加载完成之后,会进行字节码效验,字节码效验通过之后 JVM 解释器会把字节码翻译成机器码交由操作系统执行。但不是所有代码都是解释执行的,JVM 对此做了优化,比如,以 Hotspot 虚拟机来说,它本身提供了 JIT(Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热点代码编译为机器码,这个时候字节码就变成了编译执行。Java 程序执行流程图如下: + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210324143756.png) + + + +## 基础部分 + +### instanceof 关键字的作用 + +instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为: + +```java +boolean result = obj instanceof Class +``` + +其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。 + +注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。 + +```java +int i = 0; +System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型 +System.out.println(i instanceof Object);//编译不通过 +``` + +```java +Integer integer = new Integer(1); +System.out.println(integer instanceof Integer);//true +``` + +```java +//false  ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。 +System.out.println(null instanceof Object); +``` + + + +### == 和 equals 的区别是什么? + +**"=="** + +对于基本类型和引用类型 == 的作用效果是不同的,如下所示: + +- 基本类型:比较的是值是否相同; +- 引用类型:比较的是引用是否相同; + +```java +String x = "string"; +String y = "string"; +String z = new String("string"); +System.out.println(x==y); // true +System.out.println(x==z); // false +System.out.println(x.equals(y)); // true +System.out.println(x.equals(z)); // true +``` + +> 因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。 + + + +**equals** + +equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。 + +首先来看默认情况下 equals 比较一个有相同值的对象,代码如下: + +```java +class Cat { + public Cat(String name) { + this.name = name; + } + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + +Cat c1 = new Cat("叶痕秋"); +Cat c2 = new Cat("叶痕秋"); +System.out.println(c1.equals(c2)); // false +``` + +输出结果出乎我们的意料,竟然是 false?这是怎么回事,看了 equals 源码就知道了,源码如下: + +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` + +原来 equals 本质上就是 ==。 + +那问题来了,两个相同值的 String 对象,为什么返回的是 true?代码如下: + +```java +String s1 = new String("叶子"); +String s2 = new String("叶子"); +System.out.println(s1.equals(s2)); // true +``` + +同样的,当我们进入 String 的 equals 方法,找到了答案,代码如下: + +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + +原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。 + +**总结** + +**== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。** + + + +### 什么是java序列化,如何实现java序列化?或者请解释Serializable接口的作用。 + +我们有时候将一个java对象变成字节流的形式传出去或者从一个字节流中恢复成一个java对象,例如,要将java对象存储到硬盘或者传送给网络上的其他计算机,这个过程我们可以自己写代码去把一个java对象变成某个格式的字节流再传输。 + +但是,jre本身就提供了这种支持,我们可以调用`OutputStream`的`writeObject`方法来做,如果要让java帮我们做,要被传输的对象必须实现`serializable`接口,这样,javac编译时就会进行特殊处理,编译的类才可以被`writeObject`方法操作,这就是所谓的序列化。需要被序列化的类必须实现`Serializable`接口,该接口是一个mini接口,其中没有需要实现方法,implements Serializable只是为了标注该对象是可被序列化的。 + +例如,在web开发中,如果对象被保存在了Session中,tomcat在重启时要把Session对象序列化到硬盘,这个对象就必须实现Serializable接口。如果对象要经过分布式系统进行网络传输,被传输的对象就必须实现Serializable接口。 + + + +### Hashcode的作用 + +java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就会比较满。 + +于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。 + +hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。 + + + +### 两个对象的 hashCode() 相同, 那么 equals() 也一定为 true吗? + +不对,两个对象的 hashCode() 相同,equals() 不一定 true。 + +代码示例: + +```java +String str1 = "keep"; +String str2 = "brother"; +System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode())); +System. out. println(str1. equals(str2)); +``` + +执行结果: + +``` +str1:1179395 | str2:1179395 + +false +``` + +代码解读:很显然“keep”和“brother”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。 + + + +### 泛型常用特点 + +泛型是Java SE 1.5之后的特性, 《Java 核心技术》中对泛型的定义是: + +> “泛型” 意味着编写的代码可以被不同类型的对象所重用。 + +“泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素,如 + +```java +List iniData = new ArrayList<>() +``` + + + +### 使用泛型的好处? + +以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。 + + + + + +## 数据类型 + +### Java有哪些数据类型 + +Java**中**有 8 种基本数据类型,分别为: + +- **6 种数字类型 (四个整数形,两个浮点型)**:byte、short、int、long、float、double + +- **1 种字符类型**:char +- **1 种布尔型**:boolean。 + +**byte:** + +- byte 数据类型是8位、有符号的,以二进制补码表示的整数; +- 最小值是 **-128(-2^7)**; +- 最大值是 **127(2^7-1)**; +- 默认值是 **0**; +- byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一; +- 例子:byte a = 100,byte b = -50。 + +**short:** + +- short 数据类型是 16 位、有符号的以二进制补码表示的整数 +- 最小值是 **-32768(-2^15)**; +- 最大值是 **32767(2^15 - 1)**; +- Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一; +- 默认值是 **0**; +- 例子:short s = 1000,short r = -20000。 + +**int:** + +- int 数据类型是32位、有符号的以二进制补码表示的整数; +- 最小值是 **-2,147,483,648(-2^31)**; +- 最大值是 **2,147,483,647(2^31 - 1)**; +- 一般地整型变量默认为 int 类型; +- 默认值是 **0** ; +- 例子:int a = 100000, int b = -200000。 + +**long:** + +- **注意:Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析** + +- long 数据类型是 64 位、有符号的以二进制补码表示的整数; +- 最小值是 **-9,223,372,036,854,775,808(-2^63)**; +- 最大值是 **9,223,372,036,854,775,807(2^63 -1)**; +- 这种类型主要使用在需要比较大整数的系统上; +- 默认值是 **0L**; +- 例子: long a = 100000L,Long b = -200000L。 + "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。 + +**float:** + +- float 数据类型是单精度、32位、符合IEEE 754标准的浮点数; +- float 在储存大型浮点数组的时候可节省内存空间; +- 默认值是 **0.0f**; +- 浮点数不能用来表示精确的值,如货币; +- 例子:float f1 = 234.5f。 + +**double:** + +- double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数; +- 浮点数的默认类型为double类型; +- double类型同样不能表示精确的值,如货币; +- 默认值是 **0.0d**; +- 例子:double d1 = 123.4。 + +**char:** + +- char类型是一个单一的 16 位 Unicode 字符; +- 最小值是 **\u0000**(即为 0); +- 最大值是 **\uffff**(即为 65535); +- char 数据类型可以储存任何字符; +- 例子:char letter = 'A';(**单引号**) + +**boolean:** + +- boolean数据类型表示一位的信息; +- 只有两个取值:true 和 false; +- 这种类型只作为一种标志来记录 true/false 情况; +- 默认值是 **false**; +- 例子:boolean one = true。 + +**这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean** + +| 类型名称 | 字节、位数 | 最小值 | 最大值 | 默认值 | 例子 | +| :----------- | :-------------------- | :--------------------------------------------- | :------------------------------------- | :--------------- | :---------------------------------- | +| byte字节 | 1字节,8位 | -128(-2^7) | 127(2^7-1) | 0 | byte a = 100,byte b = -50 | +| short短整型 | 2字节,16位 | -32768(-2^15) | 32767(2^15 - 1) | 0 | short s = 1000,short r = -20000 | +| int整形 | 4字节,32位 | -2,147,483,648(-2^31) | 2,147,483,647(2^31 - 1) | 0 | int a = 100000, int b = -200000 | +| lang长整型 | 8字节,64位 | -9,223,372,036,854,775,808(-2^63) | 9,223,372,036,854,775,807(2^63 -1) | 0L | long a = 100000L,Long b = -200000L | +| double双精度 | 8字节,64位 | | double类型同样不能表示精确的值,如货币 | 0.0d | double d1 = 123.4 | +| float单精度 | 4字节,32位 | 在储存大型浮点数组的时候可节省内存空间 | 不同统计精准的货币值 | 0.0f | float f1 = 234.5f | +| char字符 | 2字节,16位 | \u0000(即为0) | \uffff(即为65,535) | 可以储存任何字符 | char letter = 'A'; | +| boolean布尔 | 返回true和false两个值 | 这种类型只作为一种标志来记录 true/false 情况; | 只有两个取值:true 和 false; | false | boolean one = true | + + + +### Java中引用数据类型有哪些,它们与基本数据类型有什么区别? + +引用数据类型分3种:类,接口,数组; + +**简单来说,只要不是基本数据类型.都是引用数据类型。 那他们有什么不同呢?** + + + +**1、从概念方面来说** + +1,基本数据类型:变量名指向具体的数值 + +2,引用数据类型:变量名不是指向具体的数值,而是指向存数据的内存地址,.也及时hash值 + + + +**2、从内存的构建方面来说**(内存中,有堆内存和栈内存两者) + +1,基本数据类型:被创建时,在栈内存中会被划分出一定的内存,并将数值存储在该内存中. + +2,引用数据类型:被创建时,首先会在栈内存中分配一块空间,然后在堆内存中也会分配一块具体的空间用来存储数据的具体信息,即hash值,然后由栈中引用指向堆中的对象地址. + + + +**举个例子** + +```java +//基本数据类型作为方法参数被调用 +public class Main{ + public static void main(String[] args){ + //基本数据类型 + int i = 1; + int j = 1; + double d = 1.2; + + //引用数据类型 + String str = "Hello"; + String str1= "Hello"; + } +} +``` + + + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210322011647.png) + +由上图可知,基本数据类型中会存在两个相同的1,而引用型类型就不会存在相同的数据。 +假如"hello"的引用地址是xxxxx1,声明str变量并其赋值"hello"实际上就是让str变量引用了"hello"的内存地址,这个内存地址就存储在堆内存中,是不会改变的,当再次声明变量str1也是赋值为"hello"时,此时就会在堆内存中查询是否有"hello"这个地址,如果堆内存中已经存在这个地址了,就不会再次创建了,而是让str1变量也指向xxxxx1这个地址,如果没有的话,就会重新创建一个地址给str1变量。 + + + +**从使用方面来说** + +1,基本数据类型:判断数据是否相等,用==和!=判断。 +2,引用数据类型:判断数据是否相等,用equals()方法,==和!=是比较数值的。而equals()方法是比较内存地址的。 + +**补充:数据类型选择的原则** + +- 如果要表示整数就使用int,表示小数就使用double; +- 如果要描述日期时间数字或者表示文件(或内存)大小用long; +- 如果要实现内容传递或者编码转换使用byte; +- 如果要实现逻辑的控制,可以使用booleam; +- 如果要使用中文,使用char避免中文乱码; +- 如果按照保存范围:byte < int < long < double; + + + +### Java的四种引用,强弱软虚 + +- 强引用 + + 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式: + + ```java + String str = new String("str"); + ``` + +- 软引用 + + 软引用在程序内存不足时,会被回收,使用方式: + + ```java + // 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, + // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T + SoftReference wrf = new SoftReference(new String("str")); + ``` + + 可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。 + +- 弱引用 + + 弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式: + + ```java + WeakReference wrf = new WeakReference(str); + ``` + + **可用场景:** Java源码中的`java.util.WeakHashMap`中的`key`就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。 + +- 虚引用 + + 虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入`ReferenceQueue`中。注意哦,其它引用是被JVM回收后才被传入`ReferenceQueue`中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有`ReferenceQueue`,使用例子: + + ```java + PhantomReference prf = new PhantomReference(new String("str"), new ReferenceQueue<>()); + ``` + + 可用场景: 对象销毁前的一些操作,比如说资源释放等。**`Object.finalize()`虽然也可以做这类动作,但是这个方式即不安全又低效 + + **上诉所说的几类引用,都是指对象本身的引用,而不是指`Reference`的四个子类的引用(`SoftReference`等)。** + + + +### 自动装箱与拆箱 + +* **装箱**:将基本类型用它们对应的引用类型包装起来; +* **拆箱**:将包装类型转换为基本数据类型; + + + +### 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 + + + +### Integer a= 127 与 Integer b = 127相等吗 + +* 对于对象引用类型:==比较的是对象的内存地址。 +* 对于基本数据类型:==比较的是值。 + +`如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false` + +```java +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比较 + System.out.println(b == c); // true + + Integer a1 = 128; + Integer b1 = 128; + System.out.println(a1 == b1); // false + + Integer a2 = 127; + Integer b2 = 127; + System.out.println(a2 == b2); // true +} + +``` + + + +### Java中的自动装箱与拆箱 + +**什么是自动装箱拆箱?** + +从下面的代码中就可以看到装箱和拆箱的过程 + +```java +//自动装箱 +Integer total = 99; + +//自定拆箱 +int totalprim = total; +``` + +**装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。** + +> 在Java SE5之前,自动装箱要这样写:Integer i = ``new` `Integer(``10``); + + + +对于Java的自动装箱和拆箱,我们看看源码编译后的class文件,其实装箱调用包装类的valueOf方法,拆箱调用的是Integer.Value方法,下面就是变编译后的代码: + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210322151715.png) + + + +**常见面试一:** + +这段代码输出什么? + +```java +public class Main { + public static void main(String[] args) { + + Integer i1 = 100; + Integer i2 = 100; + Integer i3 = 200; + Integer i4 = 200; + + System.out.println(i1==i2); + System.out.println(i3==i4); + } +} +``` + +答案是: + +``` +true +false +``` + +为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现: + +```java +public static Integer valueOf(int i) { + if(i >= -128 && i <= IntegerCache.high) + return IntegerCache.cache[i + 128]; + else + return new Integer(i); + } +``` + + + +```java +private static class IntegerCache { + static final int high; + static final Integer cache[]; + + static { + final int low = -128; + + // high value may be configured by property + int h = 127; + if (integerCacheHighPropValue != null) { + // Use Long.decode here to avoid invoking methods that + // require Integer's autoboxing cache to be initialized + int i = Long.decode(integerCacheHighPropValue).intValue(); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - -low); + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + } + + private IntegerCache() {} + } +``` + + + +从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。 + +上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。 + + + +**常见面试二:** + +```java +public class Main { + public static void main(String[] args) { + + Double i1 = 100.0; + Double i2 = 100.0; + Double i3 = 200.0; + Double i4 = 200.0; + + System.out.println(i1==i2); + System.out.println(i3==i4); + } +} +``` + +输出结果为: + +``` +false +false +``` + +原因很简单,在某个范围内的整型数值的个数是有限的,而浮点数却不是。 + + + +### 为什么要有包装类型? + +**让基本数据类型也具有对象的特征** + +| 基本类型 | 包装器类型 | +| :------- | :--------- | +| boolean | Boolean | +| char | Character | +| int | Integer | +| byte | Byte | +| short | Short | +| long | Long | +| float | Float | +| double | Double | + +为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型)因为容器都是装object的,这是就需要这些基本类型的包装器类了。 + +自动装箱:`new Integer(6);`,底层调用:`Integer.valueOf(6)` + +自动拆箱: `int i = new Integer(6);`,底层调用`i.intValue();`方法实现。 + +```java +Integer i = 6; +Integer j = 6; +System.out.println(i==j); +``` + +答案在下面这段代码中找: + +``` +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +**二者的区别:** + +1. 声明方式不同:基本类型不使用new关键字,而包装类型需要使用new关键字来在**堆中分配存储空间**; +2. 存储方式及位置不同:基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用; +3. 初始值不同:基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null; +4. 使用方式不同:基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。 + + + +### a=a+b与a+=b有什么区别吗? + +`+=`操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换.如: + +```java +byte a = 127; +byte b = 127; +b = a + b; // 报编译错误:cannot convert from int to byte +b += a; +``` + +以下代码是否有错,有的话怎么改? + +```java +short s1= 1; +s1 = s1 + 1; +``` + +有错误.short类型在进行运算时会自动提升为int类型,也就是说`s1+1`的运算结果是int类型,而s1是short类型,此时编译器会报错. + +正确写法: + +```java +short s1= 1; +s1 += 1; +``` + +`+=`操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错. + + + +### float f=3.4;是否正确 + +不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; + +或者写成 float f =3.4F; + + + +### 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);其中有隐含的强制类型转换。 + + + +### 能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象? + +我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,所以,如果强制转化,int 类型的高 24 位将会被丢弃,因为byte 类型的范围是从 -128 到 127 + + + +## 常用关键字 + +### final 在 Java 中有什么作用? + +final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。 + +特征:凡是引用final关键字的地方皆不可修改! + +(1)修饰类:表示该类不能被继承; + +(2)修饰方法:表示方法不能被重写; + +(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。 + + + +### 访问修饰符 public,private,protected,以及不写(默认)时的区别 + +* **定义**:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。 + +* **分类** + + * **private** : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) + + * **default** (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。 + + * **protected** : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 + + * **public** : 对所有类可见。使用对象:类、接口、变量、方法 + +**访问修饰符图** + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/14/171744c433bcfd38?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + + +### final有哪些用法? + +final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了: + +- 被final修饰的类不可以被继承 +- 被final修饰的方法不可以被重写 +- 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变. +- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率 +- 被final修饰的常量,在编译阶段会存入常量池中. + +除此之外,编译器对final域要遵守的两个重排序规则更好: + +在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序. + + + +### final 有什么用? + +用于修饰类、属性和方法 + +* 被final修饰的类不可以被继承 +* 被final修饰的方法不可以被重写 +* 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的 + +### static都有哪些用法? + +所有的人都知道static关键字这两个基本的用法:静态变量和静态方法.也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享. + +除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作: + +```java +public calss PreCache{ + static{ + //执行相关操作 + } +} +``` + +此外static也多用于修饰内部类,此时称之为静态内部类. + +最后一种用法就是静态导包,即`import static`.import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如: + +```java +import static java.lang.Math.*; + +public class Test{ + + public static void main(String[] args){ + //System.out.println(Math.sin(20));传统做法 + System.out.println(sin(20)); + } +} +``` + + + +### static存在的主要意义 + +* static的主要意义是在于创建独立于具体对象的域变量或者方法。**以致于即使没有创建对象,也能使用属性和调用方法**! +* static关键字还有一个比较关键的作用就是 **用来形成静态代码块以优化程序性能**。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 +* 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 + +### static的独特之处 + +* 1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法**不属于任何一个实例对象,而是被类的实例对象所共享**。 + +> 怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩? + +* 2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。 + +* 3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的! + +* 4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。 + + + +### static注意事项 + +* 1、静态只能访问静态。 +* 2、非静态既可以访问非静态的,也可以访问静态的。 + + + +### static和final区别 + +| 关键词 | 修饰物 | 影响 | +| :----- | :----- | :------------------------------------------------------- | +| final | 变量 | 分配到常量池中,程序不可改变其值 | +| final | 方法 | 子类中将不能被重写 | +| final | 类 | 不能被继承 | +| static | 变量 | 分配在内存堆上,引用都会指向这一个地址而不会重新分配内存 | +| static | 方法块 | 虚拟机优先加载 | +| static | 类 | 可以直接通过类来调用而不需要new | + + + +### this关键字的用法 + +* this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。 + +* this的用法在java中大体可以分为3种: + + * 1.普通的直接引用,this相当于是指向当前对象本身。 + + * 2.形参与成员名字重名,用this来区分: + + ```java + public Person(String name, int age) { + this.name = name; + this.age = age; + } + ``` + + * 3.引用本类的构造函数 + + ```java + class Person{ + private String name; + private int age; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + public Person(String name, int age) { + this(name); + this.age = age; + } + } + ``` + + + +### super关键字的用法 + +super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 + +**super也有三种用法:** + +* 1.普通的直接引用 + + 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 + + * 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 + + ```java + class Person{ + protected String name; + + public Person(String name) { + this.name = name; + } + + } + + class Student extends Person{ + private String name; + + public Student(String name, String name1) { + super(name); + this.name = name1; + } + + public void getInfo(){ + System.out.println(this.name); //Child + System.out.println(super.name); //Father + } + + } + + public class Test { + public static void main(String[] args) { + Student s1 = new Student("Father","Child"); + s1.getInfo(); + + } + } + + ``` + + 3.引用父类构造函数 + + * super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 + * this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 + + + +### &和&&的区别 + +* &运算符有两种用法:(1)按位与;(2)逻辑与。 + +* &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 + +`注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。` + + + +### break ,continue ,return 的区别及作用 + +* break 跳出总上一层循环,不再执行循环(结束当前的循环体) +* continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) +* return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) + + + +## 异常处理 + +### Java异常简介 + +Java异常是Java提供的一种识别及响应错误的一致性机制。 + +Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。 + + + +### Java异常架构 + +![image-20210814134319791](https://gitee.com/gsjqwyl/images/raw/master/uPic/tu731M.jpg) + +### Error 和 Exception 有什么区别? + +**Error ** + +表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况; + + **Exception ** + +表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。 + + + +### 阐述 final、finally、finalize 的区别 + +**1、final:** 修饰符(关键字)有三种用法:如果一个类被声明为 final,意味着它不能再派生出新的子类,即不能被继承,因此它和 abstract 是反义词。将变量声明为 final,可以保证它们在使用中不被改变,被声明为 final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为 final 的方法也同样只能使用,不能在子类中被重写。 + +**2、finally:** 通常放在 try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以将释放外部资源的代码写在 finally 块中. + +**3、finalize:** Object 类中定义的方法,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写 finalize()方法可以整理系统资源或者执行其他清理工作。 + + + +### 列出一些你常见的运行时异常? + +- `ClassCastException`(类转换异常) +- `IndexOutOfBoundsException`(数组越界) +- `NullPointerException`(空指针异常) +- `ArrayStoreException`(数据存储异常,操作数组是类型不一致) +- `BufferOverflowException` +- `ArithmeticException` 算术异常 +- `IllegalArgumentException `非法参数异常 +- `SecurityException ` 安全异常 + + + +### 什么是受检异常 + +异常表示程序运行过程中可能出现的非正常状态。 + +**运行时异常表示** 虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。 + +**受检异常** 跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。 + +Java 编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,在 Effective Java 中对异常的使用给出了以下指导原则: + +**1、** 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调用者为了正常的控制流而使用异常) + +**2、** 对可以恢复的情况使用受检异常,对编程错误使用运行时异常 + +**3、** 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生) + +**4、** 优先使用标准的异常 + +**5、** 每个方法抛出的异常都要有文档 + +**6、** 保持异常的原子性 + +**7、** 不要在 catch 中忽略掉捕获到的异常 + + + +### Excption与Error包结构 + +Java可抛出(Throwable)的结构分为三种类型:**被检查的异常(CheckedException)** ,**运行时异常(RuntimeException)** ,**错误(Error)**。 + +**1、运行时异常** + +**定义:** + +RuntimeException及其子类都被称为运行时异常。 + +**特点:** + +Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fast机制产生的ConcurrentModificationException异常(java.util包下面的所有的集合类都是快速失败的,“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制,这个错叫并发修改异常。Fail-safe,java.util.concurrent包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。)等,都属于运行时异常。 + + +**2、被检查异常** + +**定义:** Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。 + +**特点 :** Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。 + +当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。 如: + +`IOException` + +`FileNotFoundException` + +`SQLException` + +被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的`FileNotFoundException`。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的`NullPointerException`。 + +**3、错误** + +定义 : Error类及其子类。 + +特点 : 和运行时异常一样,编译器也不会对错误进行检查。 + +当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。OutOfMemoryError、ThreadDeath。 + +Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等 + + + +### try {}里有一个return语句,那么紧跟在这个try后的finally{}里的code会不会被执行,什么时候被执行,在return前还是后? + +我们知道finally{}中的语句是一定会执行的,那么这个可能正常脱口而出就是return之前,return之后可能就出了这个方法了,鬼知道跑哪里去了,但更准确的应该是在return中间执行,请看下面程序代码的运行结果: + +```java +public classTest { +    public static void main(String[]args) { +       System.out.println(newTest().test());; +    } +    static int test() +    { +       intx = 1; +       try +       { +          return x; +       } +       finally +       { +          ++x; +       } +    } +} +``` + +执行结果如下: + +``` +1 +``` + +运行结果是1,为什么呢?主函数调用子函数并得到结果的过程,好比主函数准备一个空罐子,当子函数要返回结果时,先把结果放在罐子里,然后再将程序逻辑返回到主函数。所谓返回,就是子函数说,我不运行了,你主函数继续运行吧,这没什么结果可言,结果是在说这话之前放进罐子里的。 + + + +### 运行时异常与一般异常有何异同? + +异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 + + + +### error和exception有什么区别? + +error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。exception表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。 + + + +### 简单说说Java中的异常处理机制的简单原理和应用。 + +**异常是指java程序运行时(非编译)所发生的非正常情况或错误**,与现实生活中的事件很相似,现实生活中的事件可以包含事件发生的时间、地点、人物、情节等信息,可以用一个对象来表示,Java使用面向对象的方式来处理异常,它把程序中发生的每个异常也都分别封装到一个对象来表示的,该对象中包含有异常的信息。 + + Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为java.lang.Throwable。 + +Throwable下面又派生了两个子类: + +- Error和Exception,Error表示应用程序本身无法克服和恢复的一种严重问题,程序只有奔溃了,例如,说内存溢出和线程死锁等系统问题。 + +- Exception表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常: + +系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件挂掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException); + +普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。 + +java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try..catch处理或用throws声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,而系统异常可以处理也可以不处理,所以,编译器不强制用try..catch处理或用throws声明,所以系统异常也称为unchecked异常。 + + + +## 面向对象 + +### 面向对象和面向过程的区别 + +* **面向过程**: +* 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 + +* 缺点:没有面向对象易维护、易复用、易扩展 + +* **面向对象**: + + * 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 + + * 缺点:性能比面向过程低 + +`面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。` + +`面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。` + +`面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。` + + + +### 面向对象的特征有哪些方面 + +面向对象的特征主要有以下几个方面**: + +* **抽象**:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。 +* **封装**把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 +* **继承**是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 +* 关于继承如下 3 点请记住: +* 子类拥有父类非 private 的属性和方法。 +* 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 +* 子类可以用自己的方式实现父类的方法。(以后介绍)。 +* **多态**:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 + + + +### Java多态的理解 + +1. 多态是继封装、继承之后,面向对象的第三大特性。 +2. 多态现实意义理解: + +- 现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。 +- Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一个Student的对象便既是Student,又是Person。 + +1. 多态体现为父类引用变量可以指向子类对象。 +2. 前提条件:必须有子父类关系。 + +**注意:在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法。** + +1. 多态的定义与使用格式 + +定义格式:父类类型 变量名=new 子类类型(); + + + +### 什么是多态机制?Java语言是如何实现多态的? + +* 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 + +* 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。 + +**多态的实现** + +* Java实现多态有三个必要条件:继承、重写、向上转型。 + + * 继承:在多态中必须存在有继承关系的子类和父类。 + + * 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 + + * 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 + +`只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。` + +`对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。` + + + +### 面向对象五大基本原则是什么(可选) + +* 单一职责原则SRP(Single Responsibility Principle) + 类的功能要单一,不能包罗万象,跟杂货铺似的。 +* 开放封闭原则OCP(Open-Close Principle) + 一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 +* 里式替换原则LSP(the Liskov Substitution Principle LSP) + 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~ +* 依赖倒置原则DIP(the Dependency Inversion Principle DIP) + 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 +* 接口分离原则ISP(the Interface Segregation Principle ISP) + 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。 + + + +### 抽象类和接口的对比 + +* 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 + +* 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 + +**相同点** + +* 接口和抽象类都不能实例化 +* 都位于继承的顶端,用于被其他实现或继承 +* 都包含抽象方法,其子类都必须覆写这些抽象方法 + +**不同点** + +| 参数 | 抽象类 | 接口 | +| ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 | +| 实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 | +| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | +| 访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected | +| 多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 | +| 字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 | + +**备注**:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 + +`现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。` + +* 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则: + * 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。 + * 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。 + + + +### 普通类和抽象类有哪些区别? + +* 普通类不能包含抽象方法,抽象类可以包含抽象方法。 +* 抽象类不能直接实例化,普通类可以直接实例化。 + + + +### 抽象类能使用 final 修饰吗? + +* 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类 + + + +### 创建一个对象用什么关键字?对象实例与对象引用有何不同? + +* new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球) + + + +#### 构造器(constructor)是否可被重写(override) + +* 构造器不能被继承,因此不能被重写,但可以被重载。 + + + +#### 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分? + +* 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 + +* 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 + +* 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。 + + + +## 类、变量、方法 + +### 什么是内部类? + +* 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是**内部类**。内部类本身就是类的一个属性,与其他属性定义方式一致。 + + + +### 内部类的分类有哪些 + +`内部类可以分为四种:**成员内部类、局部内部类、匿名内部类和静态内部类**。` + +##### 静态内部类 + +* 定义在类内部的静态类,就是静态内部类。 + + ``` + public class Outer { + + private static int radius = 1; + + static class StaticInner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + } + } + } + + ``` + +* 静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,`new 外部类.静态内部类()`,如下: + + ``` + Outer.StaticInner inner = new Outer.StaticInner(); + inner.visit(); + + ``` + +##### 成员内部类 + +* 定义在类内部,成员位置上的非静态类,就是成员内部类。 + + ``` + public class Outer { + + private static int radius = 1; + private int count =2; + + class Inner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + System.out.println("visit outer variable:" + count); + } + } + } + + ``` + +* 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式`外部类实例.new 内部类()`,如下: + + ``` + Outer outer = new Outer(); + Outer.Inner inner = outer.new Inner(); + inner.visit(); + + ``` + +##### 局部内部类 + +* 定义在方法中的内部类,就是局部内部类。 + + ``` + public class Outer { + + private int out_a = 1; + private static int STATIC_b = 2; + + public void testFunctionClass(){ + int inner_c =3; + class Inner { + private void fun(){ + System.out.println(out_a); + System.out.println(STATIC_b); + System.out.println(inner_c); + } + } + Inner inner = new Inner(); + inner.fun(); + } + public static void testStaticFunctionClass(){ + int d =3; + class Inner { + private void fun(){ + // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 + System.out.println(STATIC_b); + System.out.println(d); + } + } + Inner inner = new Inner(); + inner.fun(); + } + } + + ``` + +* 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,`new 内部类()`,如下: + + ``` + public static void testStaticFunctionClass(){ + class Inner { + } + Inner inner = new Inner(); + } + + ``` + +##### 匿名内部类 + +* 匿名内部类就是没有名字的内部类,日常开发中使用的比较多。 + + ``` + public class Outer { + + private void test(final int i) { + new Service() { + public void method() { + for (int j = 0; j < i; j++) { + System.out.println("匿名内部类" ); + } + } + }.method(); + } + } + //匿名内部类必须继承或实现一个已有的接口 + interface Service{ + void method(); + } + + ``` + +* 除了没有名字,匿名内部类还有以下特点: + + * 匿名内部类必须继承一个抽象类或者实现一个接口。 + * 匿名内部类不能定义任何静态成员和静态方法。 + * 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 + * 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 + +* 匿名内部类创建方式: + + ``` + new 类/接口{ + //匿名内部类实现部分 + } + + ``` + + + +### 内部类的优点 + +`我们为什么要使用内部类呢?因为它有以下优点:` + +* 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! +* 内部类不为同一包的其他类所见,具有很好的封装性; +* 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 +* 匿名内部类可以很方便的定义回调。 + + + +### 内部类有哪些应用场景 + +1. 一些多算法场合 +2. 解决一些非面向对象的语句块。 +3. 适当使用内部类,使得代码更加灵活和富有扩展性。 +4. 当某个类除了它的外部类,不再被其他的类使用时。 + + + +### 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final? + +* 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码: + + ``` + public class Outer { + + void outMethod(){ + final int a =10; + class Inner { + void innerMethod(){ + System.out.println(a); + } + } + } + } + + ``` + +* 以上例子,为什么要加final呢?是因为**生命周期不一致**, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。 + + + +### 内部类相关,看程序说出运行结果 + +``` +public class Outer { + private int age = 12; + + class Inner { + private int age = 13; + public void print() { + int age = 14; + System.out.println("局部变量:" + age); + System.out.println("内部类变量:" + this.age); + System.out.println("外部类变量:" + Outer.this.age); + } + } + + public static void main(String[] args) { + Outer.Inner in = new Outer().new Inner(); + in.print(); + } + +} + +``` + +运行结果: + +``` +局部变量:14 +内部类变量:13 +外部类变量:12 + +``` + + + +### 成员变量与局部变量的区别有哪些 + +* 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域 + +* 成员变量:方法外部,类内部定义的变量 + +* 局部变量:类的方法中的变量。 + +* 成员变量和局部变量的区别 + +**作用域** + +* 成员变量:针对整个类有效。 +* 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内) + +**存储位置** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。 +* 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。 + +**生命周期** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失 +* 局部变量:当方法调用完,或者语句结束后,就自动释放。 + +**初始值** + +* 成员变量:有默认初始值。 +* 局部变量:没有默认初始值,使用前必须赋值。 + + + +### 在Java中定义一个不做事且没有参数的构造方法的作用 + +* Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 + + + +### 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? + +* 帮助子类做初始化工作。 + + + +### 一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么? + +* 主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 + + + +### 构造方法有哪些特性? + +* 名字与类名相同; + +* 没有返回值,但不能用void声明构造函数; + +* 生成类的对象时自动执行,无需调用。 + + + +### 静态变量和实例变量区别 + +* 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。 + +* 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。 + + + +### 静态变量与普通变量区别 + +* static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 + +* 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。 + + + +### 静态方法和实例方法有何不同? + +`静态方法和实例方法的区别主要体现在两个方面:` + +* 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 + +* 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 + + + +### 在一个静态方法内调用一个非静态成员为什么是非法的? + +* 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 + + + +### 什么是方法的返回值?返回值的作用是什么? + +* 方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! + + + +### 什么是内部类? + +* 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是**内部类**。内部类本身就是类的一个属性,与其他属性定义方式一致。 + + + +### 内部类的分类有哪些 + +`内部类可以分为四种:**成员内部类、局部内部类、匿名内部类和静态内部类**。` + +##### 静态内部类 + +* 定义在类内部的静态类,就是静态内部类。 + + ``` + public class Outer { + + private static int radius = 1; + + static class StaticInner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + } + } + } + + ``` + +* 静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,`new 外部类.静态内部类()`,如下: + + ``` + Outer.StaticInner inner = new Outer.StaticInner(); + inner.visit(); + + ``` + +##### 成员内部类 + +* 定义在类内部,成员位置上的非静态类,就是成员内部类。 + + ``` + public class Outer { + + private static int radius = 1; + private int count =2; + + class Inner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + System.out.println("visit outer variable:" + count); + } + } + } + + ``` + +* 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式`外部类实例.new 内部类()`,如下: + + ``` + Outer outer = new Outer(); + Outer.Inner inner = outer.new Inner(); + inner.visit(); + + ``` + +##### 局部内部类 + +* 定义在方法中的内部类,就是局部内部类。 + + ``` + public class Outer { + + private int out_a = 1; + private static int STATIC_b = 2; + + public void testFunctionClass(){ + int inner_c =3; + class Inner { + private void fun(){ + System.out.println(out_a); + System.out.println(STATIC_b); + System.out.println(inner_c); + } + } + Inner inner = new Inner(); + inner.fun(); + } + public static void testStaticFunctionClass(){ + int d =3; + class Inner { + private void fun(){ + // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 + System.out.println(STATIC_b); + System.out.println(d); + } + } + Inner inner = new Inner(); + inner.fun(); + } + } + + ``` + +* 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,`new 内部类()`,如下: + + ``` + public static void testStaticFunctionClass(){ + class Inner { + } + Inner inner = new Inner(); + } + + ``` + +##### 匿名内部类 + +* 匿名内部类就是没有名字的内部类,日常开发中使用的比较多。 + + ``` + public class Outer { + + private void test(final int i) { + new Service() { + public void method() { + for (int j = 0; j < i; j++) { + System.out.println("匿名内部类" ); + } + } + }.method(); + } + } + //匿名内部类必须继承或实现一个已有的接口 + interface Service{ + void method(); + } + + ``` + +* 除了没有名字,匿名内部类还有以下特点: + + * 匿名内部类必须继承一个抽象类或者实现一个接口。 + * 匿名内部类不能定义任何静态成员和静态方法。 + * 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 + * 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 + +* 匿名内部类创建方式: + + ``` + new 类/接口{ + //匿名内部类实现部分 + } + + ``` + + + +### 内部类的优点 + +`我们为什么要使用内部类呢?因为它有以下优点:` + +* 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! +* 内部类不为同一包的其他类所见,具有很好的封装性; +* 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 +* 匿名内部类可以很方便的定义回调。 + + + +### 内部类有哪些应用场景 + +1. 一些多算法场合 +2. 解决一些非面向对象的语句块。 +3. 适当使用内部类,使得代码更加灵活和富有扩展性。 +4. 当某个类除了它的外部类,不再被其他的类使用时。 + + + +### 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final? + +* 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码: + + ``` + public class Outer { + + void outMethod(){ + final int a =10; + class Inner { + void innerMethod(){ + System.out.println(a); + } + } + } + } + + ``` + +* 以上例子,为什么要加final呢?是因为**生命周期不一致**, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。 + + + +### 内部类相关,看程序说出运行结果 + +``` +public class Outer { + private int age = 12; + + class Inner { + private int age = 13; + public void print() { + int age = 14; + System.out.println("局部变量:" + age); + System.out.println("内部类变量:" + this.age); + System.out.println("外部类变量:" + Outer.this.age); + } + } + + public static void main(String[] args) { + Outer.Inner in = new Outer().new Inner(); + in.print(); + } + +} + +``` + +运行结果: + +``` +局部变量:14 +内部类变量:13 +外部类变量:12 + +``` + + + + + +## 文件、I/O流 + +### Java 中有几种类型的流? + +从输入输出方面来讲: Java中有输入流和输出流 + +从流的编码方式上来讲: Java中有字节流和字符流 + + + +对于字节流而言:主要继承的抽象类为 InputStream和OutputStream + +对于字符流而言:主要继承的抽象类为 InputStreamReader和OutputStreamReder + + + +### 写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。 + +```java + public int countWords(String file, String find) throws Exception { + int count = 0; + Reader in = new FileReader(file); + int c; + while ((c = in.read()) != -1) { + while (c == find.charAt(0)) { + for (int i = 1; i < find.length(); i++) { + c = in.read(); + if (c != find.charAt(i)){ + break; + } + if (i == find.length() - 1){ + count++; + } + } + } + } + return count; + } +``` + + + +### Java 中怎么创建 ByteBuffer? + +```java +byte[] bytes = new byte[10]; + +ByteBuffer buf = ByteBuffer.wrap(bytes); +``` + + + +### 说出 5 条 IO 的最佳实践(答案) + +IO 对 Java 应用的性能非常重要。理想情况下,你不应该在你应用的关键路径上 + +避免 IO 操作。下面是一些你应该遵循的 Java IO 最佳实践: + +**1、** 使用有缓冲区的 IO 类,而不要单独读取字节或字符。 + +**2、** 使用 NIO 和 NIO2 + +**3、** 在 finally 块中关闭流,或者使用 try-with-resource 语句。 + +**4、** 使用内存映射文件获取更快的 IO。 + + + +### Java 中 IO 流分为几种? + +* 按照流的流向分,可以分为输入流和输出流; +* 按照操作单元划分,可以划分为字节流和字符流; +* 按照流的角色划分为节点流和处理流。 + +`Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。` + +* InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +* OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +`按操作方式分类结构图:` + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/14/171744c4799a7a74?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +`按操作对象分类结构图:` + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/14/171744c479a04121?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + + +### 谈谈Java IO里面的常见类,字节流,字符流、接口、实现类、方法阻塞 + +输入流就是从外部文件输入到内存,输出流主要是从内存输出到文件。 +IO里面常见的类,第一印象就只知道IO流中有很多类,IO流主要分为字符流和字节流。 + +字符流中有抽象类InputStream和OutputStream,它们的子类FileInputStream,FileOutputStream,BufferedOutputStream等。 + +字符流BufferedReader和Writer等。都实现了Closeable, Flushable, Appendable这些接口。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。 +java中的阻塞式方法是指在程序调用改方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如read()和readLine()方法。 + + + +### 字符流和字节流有什么区别? + +字节流用于操作包含ASCII字符的文件。JAVA也支持其他的字符如Unicode,为了读取包含Unicode字符的文件,JAVA语言引入了字符流。ASCII作为Unicode的子集,对于英语字符的文件,可以使用字节流也可以使用字符流。 + + + +### 字节流和字符流,你更喜欢哪一个? + +更喜欢使用字符流。许多在字符流中存在的特性,字节流中不存在。比如使用BufferedReader而不是BufferedInputStream或DataInputStream,它其中包含一个`readLine()` 方法用于读取文本行;又比如BufferedWriter流中有一个独特的向文件写入一个换行符的方法‘newLine()’用来读取下一行,但是在字节流中我们需要做额外的操作。 + + + +### System.out.println()是什么? + +println是PrintStream的一个方法。out是一个静态PrintStream类型的成员变量,System是一个java.lang包中的类,用于和底层的操作系统进行交互。 + + + +### 什么是Filter流? + +**1、** Filter Stream是一种IO流。 +**2、** Filter流的主要作用是:**对存在的流增加一些额外的功能**,像给目标文件增加源文件中不存在的行数,或者增加拷贝的性能。 + +Filter Stream是一种IO流。Filter流的主要作用是:对存在的流增加一些额外的功能,像给目标文件增加源文件中不存在的行数,或者增加拷贝的性能。在java.io包中主要由4个可用的filter Stream组成。两个字节filter stream,两个字符filter stream。分别是:FilterInputStream、FilterOutputStream、FilterReader和FilterWriter。 + + + +### 有哪些可用的Filter流? + +在java.io包中主要由4个可用的filter Stream组成。两个字节filter stream,两个字符filter stream。分别是:`FilterInputStream`、`FilterOutputStream`、`FilterReader`和`FilterWriter`。 + + + +### 有哪些Filter流的子类? + +**1、** `LineNumberInputStream`:给目标文件增加行号。 +**2、** `DataInputStream`:有些特殊的方法如:readInt()、readDouble()和readLine()等可以一次性的读取一个int, double和一个string类型的数据。 +**3、** `BufferedInputStream`:增加性能。 +**4、** `PushbackInputStream`:推送要求的字节到系统中。 + + + +### 在**文件拷贝**的时候,哪一种流可用于提升更多的性能? + +**1、** 在字节流的时候,使用BufferedInputStream和BufferedOutputStream。 +**2、** 在字符流的时候,使用BufferedReader和BufferedWriter。 + + + +### Java中流类的**超类**(**均为抽象类**)主要由哪些组成? + +**1、** `java.io.InputStream`(字节输入流) +**2、** `java.io.OutputStream`(字节输出流) +**3、** `java.io.Reader`(字符输入流) +**4、** `java.io.Writer`(字符输出流) + + + +### FileInputStream和FileOutputStream是什么? + +**1、** 这是在拷贝文件操作的时候,经常用到的两个类。 +**2、** 在处理小文件的时候,它们的性能表现还不错,在大文件的时候,最好使用`BufferedInputStream`(或`BufferedReader`)和`BufferedOutputStream`(或`BufferedWriter`) + + + +### BIO、NIO、AIO 有什么区别? + +关于Java中的IO,网络通讯模型主要分三种:IO、NIO和AIO。 + +![img](https://img-blog.csdnimg.cn/2020022721453770.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NoaXBmZWlfY3Nkbg==,size_16,color_FFFFFF,t_70) + +**适用场景:** + +**BIO,** 适用于连接较少,对服务器资源消耗很大,但是编程简单。是同步阻塞的。 +举例:你到餐馆点餐,然后在那儿等着,什么也做不了,只要饭还没有好,就要必须等着 +**NIO,** 使用于连接数量比较多且连接时间比较短的架构,比如聊天服务器,编程比较复杂。是同步非阻塞的 +举例:你到餐馆点完餐,然后就可以去玩儿了,玩一会儿就回饭馆问一声,饭好了没。 +**AIO,** 适用于连接数量多而且连接时间长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。是异步非阻塞的。 +举例:饭馆打电话给你说,我们知道你的位置,待会儿给您送来,你安心的玩儿就可以了。类似于外卖。 + +**1、IO(同步阻塞)** + +传统的网络通讯模型,就是BIO,同步阻塞IO, 其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket去连接服务端的那个ServerSocket, ServerSocket接收到了一个的连接请求就创建一个Socket和一个线程去跟那个Socket进行通讯。接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端Socket进行处理后返回响应,在响应返回前,客户端那边就阻塞等待,上门事情也做不了。 这种方式的缺点, 每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端,这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。 + +**2、NIO(同步非阻塞,JDK1.4)** + +NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。 + +**3、AIO(NIO2,异步非阻塞,JDK1.7)** + +对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。 + +与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道: + +- AsynchronousSocketChannel + +- AsynchronousServerSocketChannel + +- AsynchronousFileChannel + +- AsynchronousDatagramChannel + + + AIO是异步非阻塞IO,基于Proactor模型实现。 每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情,等到操作系统完成读之后,就会调用你的接口,给你操作系统异步读完的数据。这个时候你就可以拿到数据进行处理,将数据往回写,在往回写的过程,同样是给操作系统一个Buffer,让操作系统去完成写,写完了来通知你。这俩个过程都有buffer存在,数据都是通过buffer来完成读写。, + + + +### 讲讲NIO + +看了一些文章,传统的IO流是阻塞式的,会一直监听一个ServerSocket,在调用read等方法时,他会一直等到数据到来或者缓冲区已满时才返回。 + +调用accept也是一直阻塞到有客户端连接才会返回。每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。并且多线程处理多个连接。每个线程拥有自己的栈空间并且占用一些 CPU 时间。每个线程遇到外部未准备好的时候,都会阻塞掉。阻塞的结果就是会带来大量的进程上下文切换。 +对于NIO,它是非阻塞式,核心类: +**1、** Buffer为所有的原始类型提供 (Buffer)缓存支持。 +**2、** Charset字符集编码解码解决方案 +**3、** Channel一个新的原始 I/O抽象,用于读写Buffer类型,通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。 + + + +### Files的常用方法都有哪些? + +- Files. exists():检测文件路径是否存在。 +- Files. createFile():创建文件。 +- Files. createDirectory():创建文件夹。 +- Files. delete():删除一个文件或目录。 +- Files. copy():复制文件。 +- Files. move():移动文件。 +- Files. size():查看文件个数。 +- Files. read():读取文件。 +- Files. write():写入文件。 + + + +### Java 中 IO 流分为几种? + +- 按照流的流向分,可以分为输入流和输出流; +- 按照操作单元划分,可以划分为字节流和字符流; +- 按照流的角色划分为节点流和处理流。 + +Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。 + +- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +按操作方式分类结构图: + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210324010753.png) + + + +### 常见的NIO框架有哪些 + +**1、Mina** + +Mina是 Apache组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 Mina 版本2.04支持基于 JavaNIO 技术的 TCP/UDP 应用程序开发、串口通讯程序,Mina 所支持的功能也在进一步的扩展中。 + +**2、Netty** + +Netty是一款异步的事件驱动的网络应用框架和工具,用于快速开发可维护的高性能、高扩展性协议服务器和客户端。也就是说,Netty是一个NIO客户端/服务器框架,支持快速、简单地开发网络应用,如协议服务器和客户端。它极大简化了网络编程,如TCP和UDP套接字服务器。 + +**3、Grizzly** + +Grizzly是一种应用程序框架,专门解决编写成千上万用户访问服务器时候产生的各种问题。使用JAVANIO作为基础,并隐藏其编程的复杂性。容易使用的高性能的API。带来非阻塞socketd到协议处理层。利用高性能的缓冲和缓冲管理使用高性能的线程池。 + + + +### Java IO 中的设计模式?(重点) + +重点是用到两个设计模式:装饰模式和适配器模式。 + +**装饰模式:** 在由InputStream、OutputStream、Reader和Writer代表的等级结构内部,有一些流处理器可以对另一些流处理器起到装饰作用,形成新的、具有改善了的功能的流处理器。 +**适配器模式:** 在由InputStream、OutputStream、Reader和Writer代表的等级结构内部,有一些流处理器是对其他类型的流处理器的适配,这就是适配器的应用。 + + + +### 在文件拷贝的时候,哪一种流可用于提升更多的性能? + +- 在字节流的时候,使用BufferedInputStream和BufferedOutputStream。 +- 在字符流的时候,使用BufferedReader和BufferedWriter。 + + + +### 说说管道流(Piped Stream) + +- 有四种管道流:`PipedInputStream`、`PipedOutputStream`、`PipedReader`和`PipedWriter`。 +- 在多个线程或进程中传递数据的时候管道流非常有用。 + + + +### 说说File类 + +- 它不属于IO流,也不是用于文件操作的。 +- 它主要是用于获取一个文件的属性、读写权限、大小等信息 + + + +## 反射 + +### 什么是反射机制? + +* JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 + +* 静态编译和动态编译 + + * 静态编译:在编译时确定类型,绑定对象 + + * 动态编译:运行时确定类型,绑定对象 + + + +### 反射机制优缺点 + +* **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 +* **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。 + + + +### 反射机制的应用场景有哪些? + +* 反射是框架设计的灵魂。 + +* 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 + +* 举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 + + + +### Java获取反射的三种方法 + +1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制 + +``` +public class Student { + private int id; + String name; + protected boolean sex; + public float score; +} + +public class Get { + //获取反射机制三种方式 + public static void main(String[] args) throws ClassNotFoundException { + //方式一(通过建立对象) + Student stu = new Student(); + Class classobj1 = stu.getClass(); + System.out.println(classobj1.getName()); + //方式二(所在通过路径-相对路径) + Class classobj2 = Class.forName("fanshe.Student"); + System.out.println(classobj2.getName()); + //方式三(通过类名) + Class classobj3 = Student.class; + System.out.println(classobj3.getName()); + } +} + +``` + + + +## 字符串 + +### 字符型常量和字符串常量的区别 + +1. 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符 +2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置) +3. 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志) + + + +### 什么是字符串常量池? + +* 字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。 + + + +### String 是最基本的数据类型吗 + +* 不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。 + +`这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char\[\] chars = {‘你’,‘好’};` + +`但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。` + + + +### String有哪些特性 + +* 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。 + +* 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。 + +* final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。 + + + +### String为什么是不可变的吗? + +* 简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以: + + /** The value is used for character storage. */ private final char value[]; + + + +### String真的是不可变的吗? + +* 我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子: + +**1 String不可变但不代表引用不可以变** + +``` +String str = "Hello"; +str = str + " World"; +System.out.println("str=" + str); + +``` + +* 结果: + + str=Hello World + +* 解析: + +* 实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 + +**2.通过反射是可以修改所谓的“不可变”对象** + +``` +// 创建字符串"Hello World", 并赋给引用s +String s = "Hello World"; + +System.out.println("s = " + s); // Hello World + +// 获取String类中的value字段 +Field valueFieldOfString = String.class.getDeclaredField("value"); + +// 改变value属性的访问权限 +valueFieldOfString.setAccessible(true); + +// 获取s对象上的value属性的值 +char[] value = (char[]) valueFieldOfString.get(s); + +// 改变value所引用的数组中的第5个字符 +value[5] = '_'; + +System.out.println("s = " + s); // Hello_World + +``` + +* 结果: + + s = Hello World s = Hello_World + +* 解析: + +* 用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 + + + +### 是否可以继承 String 类 + +* String 类是 final 类,不可以被继承。 + + + +### String str="i"与 String str=new String(“i”)一样吗? + +* 不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 + + + +### String s = new String(“xyz”);创建了几个字符串对象 + +两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。 + + + +### 如何将字符串反转? + +使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。 + + + + + +### 数组有没有 length()方法?String 有没有 length()方法 + +* 数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。 + + + +### String 类的常用方法都有那些? + +* indexOf():返回指定字符的索引。 +* charAt():返回指定索引处的字符。 +* replace():字符串替换。 +* trim():去除字符串两端空白。 +* split():分割字符串,返回一个分割后的字符串数组。 +* getBytes():返回字符串的 byte 类型数组。 +* length():返回字符串长度。 +* toLowerCase():将字符串转成小写字母。 +* toUpperCase():将字符串转成大写字符。 +* substring():截取字符串。 +* equals():字符串比较。 + + + +### 在使用 HashMap 的时候,用 String 做 key 有什么好处? + +* HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。 + + + +### String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的 + +**可变性** + +* String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。 + +**线程安全性** + +* String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +* 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结** + +* 如果要操作少量的数据用 = String + +* 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder + +* 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer + diff --git "a/interviewDoc/Java/base/Java\345\271\266\345\217\221\347\274\226\347\250\213.md" "b/interviewDoc/Java/base/Java\345\271\266\345\217\221\347\274\226\347\250\213.md" new file mode 100644 index 0000000..39f840c --- /dev/null +++ "b/interviewDoc/Java/base/Java\345\271\266\345\217\221\347\274\226\347\250\213.md" @@ -0,0 +1,1901 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + + + +- [为什么要使用并发编程](#为什么要使用并发编程) + - [多线程应用场景](#多线程应用场景) + - [并发编程有什么缺点](#并发编程有什么缺点) + - [并发编程三个必要因素是什么?](#并发编程三个必要因素是什么) + - [Java 程序中怎么保证多线程的运行安全?](#java-程序中怎么保证多线程的运行安全) + - [并行和并发有什么区别?](#并行和并发有什么区别) + - [什么是多线程](#什么是多线程) + - [多线程的好处](#多线程的好处) + - [多线程的劣势:](#多线程的劣势) + - [线程和进程区别](#线程和进程区别) + - [什么是上下文切换?](#什么是上下文切换) + - [守护线程和用户线程有什么区别呢?](#守护线程和用户线程有什么区别呢) + - [如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?](#如何在-windows-和-linux-上查找哪个线程cpu利用率最高) + - [什么是线程死锁](#什么是线程死锁) + - [形成死锁的四个必要条件是什么](#形成死锁的四个必要条件是什么) + - [如何避免线程死锁](#如何避免线程死锁) + - [创建线程的四种方式](#创建线程的四种方式) + - [说一下 runnable 和 callable 有什么区别](#说一下-runnable-和-callable-有什么区别) + - [线程的 run()和 start()有什么区别?](#线程的-run和-start有什么区别) + - [为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) + - [什么是 Callable 和 Future?](#什么是-callable-和-future) + - [什么是 FutureTask](#什么是-futuretask) + - [线程的状态](#线程的状态) + - [Java 中用到的线程调度算法是什么?](#java-中用到的线程调度算法是什么) + - [线程的调度策略](#线程的调度策略) + - [什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?](#什么是线程调度器thread-scheduler和时间分片time-slicing-) + - [请说出与线程同步以及线程调度相关的方法。](#请说出与线程同步以及线程调度相关的方法) + - [sleep() 和 wait() 有什么区别?](#sleep-和-wait-有什么区别) + - [你是如何调用 wait() 方法的?使用 if 块还是循环?为什么?](#你是如何调用-wait-方法的使用-if-块还是循环为什么) + - [为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?](#为什么线程通信的方法-wait-notify和-notifyall被定义在-object-类里) + - [为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?](#为什么-wait-notify和-notifyall必须在同步方法或者同步块中被调用) + - [Thread 类中的 yield 方法有什么作用?](#thread-类中的-yield-方法有什么作用) + - [为什么 Thread 类的 sleep()和 yield ()方法是静态的?](#为什么-thread-类的-sleep和-yield-方法是静态的) + - [线程的 sleep()方法和 yield()方法有什么区别?](#线程的-sleep方法和-yield方法有什么区别) + - [如何停止一个正在运行的线程?](#如何停止一个正在运行的线程) + - [Java 中 interrupted 和 isInterrupted 方法的区别?](#java-中-interrupted-和-isinterrupted-方法的区别) + - [什么是阻塞式方法?](#什么是阻塞式方法) + - [Java 中你怎样唤醒一个阻塞的线程?](#java-中你怎样唤醒一个阻塞的线程) + - [notify() 和 notifyAll() 有什么区别?](#notify-和-notifyall-有什么区别) + - [如何在两个线程间共享数据?](#如何在两个线程间共享数据) + - [Java 如何实现多线程之间的通讯和协作?](#java-如何实现多线程之间的通讯和协作) + - [同步方法和同步块,哪个是更好的选择?](#同步方法和同步块哪个是更好的选择) + - [什么是线程同步和线程互斥,有哪几种实现方式?](#什么是线程同步和线程互斥有哪几种实现方式) + - [在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?](#在监视器monitor内部是如何做线程同步的程序应该做哪种级别的同步) + - [如果你提交任务时,线程池队列已满,这时会发生什么](#如果你提交任务时线程池队列已满这时会发生什么) + - [什么叫线程安全?servlet 是线程安全吗?](#什么叫线程安全servlet-是线程安全吗) + - [在 Java 程序中怎么保证多线程的运行安全?](#在-java-程序中怎么保证多线程的运行安全) + - [你对线程优先级的理解是什么?](#你对线程优先级的理解是什么) + - [线程类的构造方法、静态块是被哪个线程调用的](#线程类的构造方法静态块是被哪个线程调用的) + - [Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈?](#java-中怎么获取一份线程-dump-文件你如何在-java-中获取线程堆栈) + - [一个线程运行时发生异常会怎样?](#一个线程运行时发生异常会怎样) + - [Java 线程数过多会造成什么异常?](#java-线程数过多会造成什么异常) + - [多线程的常用方法](#多线程的常用方法) +- [并发理论](#并发理论) + - [Java中垃圾回收有什么目的?什么时候进行垃圾回收?](#java中垃圾回收有什么目的什么时候进行垃圾回收) + - [线程之间如何通信及线程之间如何同步](#线程之间如何通信及线程之间如何同步) + - [Java内存模型](#java内存模型) + - [如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?](#如果对象的引用被置为null垃圾收集器是否会立即释放对象占用的内存) + - [finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?](#finalize方法什么时候被调用析构函数finalization的目的是什么) + - [什么是重排序](#什么是重排序) + - [重排序实际执行的指令步骤](#重排序实际执行的指令步骤) + - [重排序遵守的规则](#重排序遵守的规则) + - [as-if-serial规则和happens-before规则的区别](#as-if-serial规则和happens-before规则的区别) + - [并发关键字 synchronized ?](#并发关键字-synchronized-) + - [说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#说说自己是怎么使用-synchronized-关键字在项目中用到了吗) + - [单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!](#单例模式了解吗给我解释一下双重检验锁方式实现单例模式) + - [说一下 synchronized 底层实现原理?](#说一下-synchronized-底层实现原理) + - [synchronized可重入的原理](#synchronized可重入的原理) + - [什么是自旋](#什么是自旋) + - [多线程中 synchronized 锁升级的原理是什么?](#多线程中-synchronized-锁升级的原理是什么) + - [线程 B 怎么知道线程 A 修改了变量](#线程-b-怎么知道线程-a-修改了变量) + - [当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?](#当一个线程进入一个对象的-synchronized-方法-a-之后其它线程是否可进入此对象的-synchronized-方法-b) + - [synchronized、volatile、CAS 比较](#synchronizedvolatilecas-比较) + - [synchronized 和 Lock 有什么区别?](#synchronized-和-lock-有什么区别) + - [synchronized 和 ReentrantLock 区别是什么?](#synchronized-和-reentrantlock-区别是什么) + - [volatile 关键字的作用](#volatile-关键字的作用) + - [Java 中能创建 volatile 数组吗?](#java-中能创建-volatile-数组吗) + - [volatile 变量和 atomic 变量有什么不同?](#volatile-变量和-atomic-变量有什么不同) + - [volatile 能使得一个非原子操作变成原子操作吗?](#volatile-能使得一个非原子操作变成原子操作吗) + - [synchronized 和 volatile 的区别是什么?](#synchronized-和-volatile-的区别是什么) + - [final不可变对象,它对写并发应用有什么帮助?](#final不可变对象它对写并发应用有什么帮助) + - [Lock 接口和synchronized 对比同步它有什么优势?](#lock-接口和synchronized-对比同步它有什么优势) + - [乐观锁和悲观锁的理解及如何实现,有哪些实现方式?](#乐观锁和悲观锁的理解及如何实现有哪些实现方式) + - [什么是 CAS](#什么是-cas) + - [CAS 的会产生什么问题?](#cas-的会产生什么问题) + - [什么是原子类](#什么是原子类) + - [原子类的常用类](#原子类的常用类) + - [说一下 Atomic的原理?](#说一下-atomic的原理) + - [死锁与活锁的区别,死锁与饥饿的区别?](#死锁与活锁的区别死锁与饥饿的区别) +- [线程池](#线程池) + - [什么是线程池?](#什么是线程池) + - [线程池作用?](#线程池作用) + - [线程池有什么优点?](#线程池有什么优点) + - [什么是ThreadPoolExecutor?](#什么是threadpoolexecutor) + - [什么是Executors?](#什么是executors) + - [线程池四种创建方式?](#线程池四种创建方式) + - [在 Java 中 Executor 和 Executors 的区别?](#在-java-中-executor-和-executors-的区别) + - [四种构建线程池的区别及特点?](#四种构建线程池的区别及特点) + - [线程池都有哪些状态?](#线程池都有哪些状态) + - [线程池中 submit() 和 execute() 方法有什么区别?](#线程池中-submit-和-execute-方法有什么区别) + - [什么是线程组,为什么在 Java 中不推荐使用?](#什么是线程组为什么在-java-中不推荐使用) + - [ThreadPoolExecutor饱和策略有哪些?](#threadpoolexecutor饱和策略有哪些) + - [如何自定义线程线程池?](#如何自定义线程线程池) + - [线程池的执行原理?](#线程池的执行原理) + - [如何合理分配线程池大小?](#如何合理分配线程池大小) + - [什么是CPU密集](#什么是cpu密集) + - [什么是IO密集](#什么是io密集) + - [分配CPU和IO密集:](#分配cpu和io密集) +- [并发容器](#并发容器) + - [你经常使用什么并发容器,为什么?](#你经常使用什么并发容器为什么) + - [什么是Vector](#什么是vector) + - [ArrayList和Vector有什么不同之处?](#arraylist和vector有什么不同之处) + - [为什么HashTable是线程安全的?](#为什么hashtable是线程安全的) + - [用过ConcurrentHashMap,讲一下他和HashTable的不同之处?](#用过concurrenthashmap讲一下他和hashtable的不同之处) + - [Collections.synchronized * 是什么?](#collectionssynchronized--是什么) + - [Java 中 ConcurrentHashMap 的并发度是什么?](#java-中-concurrenthashmap-的并发度是什么) + - [什么是并发容器的实现?](#什么是并发容器的实现) + - [Java 中的同步集合与并发集合有什么区别?](#java-中的同步集合与并发集合有什么区别) + - [SynchronizedMap 和 ConcurrentHashMap 有什么区别?](#synchronizedmap-和-concurrenthashmap-有什么区别) + - [CopyOnWriteArrayList 是什么?](#copyonwritearraylist-是什么) + - [CopyOnWriteArrayList 的使用场景?](#copyonwritearraylist-的使用场景) + - [CopyOnWriteArrayList 的缺点?](#copyonwritearraylist-的缺点) + - [CopyOnWriteArrayList 的设计思想?](#copyonwritearraylist-的设计思想) +- [并发队列](#并发队列) + - [什么是并发队列:](#什么是并发队列) + - [并发队列和并发集合的区别:](#并发队列和并发集合的区别) + - [怎么判断并发队列是阻塞队列还是非阻塞队列](#怎么判断并发队列是阻塞队列还是非阻塞队列) + - [阻塞队列和非阻塞队列区别](#阻塞队列和非阻塞队列区别) + - [常用并发列队的介绍:](#常用并发列队的介绍) + - [并发队列的常用方法](#并发队列的常用方法) +- [并发工具类](#并发工具类) + - [常用的并发工具类有哪些?](#常用的并发工具类有哪些) + + + + +### 为什么要使用并发编程 + +* 提升多核CPU的利用率:一般来说一台主机上的会有多个CPU核心,我们可以创建多个线程,理论上讲操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU的使用效率,如果使用单线程就只能有一个CPU核心被使用。 + +* 比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。 + +* 简单来说就是: + + * 充分利用多核CPU的计算能力; + * 方便进行业务拆分,提升应用性能 + + + +### 多线程应用场景 + +* 例如: 迅雷多线程下载、数据库连接池、分批发送短信等。 + + + +### 并发编程有什么缺点 + +* 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。 + + + +### 并发编程三个必要因素是什么? + +* 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 +* 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile) +* 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) + + + +### Java 程序中怎么保证多线程的运行安全? + +* 出现线程安全问题的原因一般都是三个原因: + + * 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)。 + + * 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题 + + * 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题 + + + +### 并行和并发有什么区别? + +* 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。 +* 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 +* 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。 + +**做一个形象的比喻:** + +* 并发 = 俩个人用一台电脑。 + +* 并行 = 俩个人分配了俩台电脑。 + +* 串行 = 俩个人排队使用一台电脑。 + + + +### 什么是多线程 + +* 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。 + + + +### 多线程的好处 + +* 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。 + + + +### 多线程的劣势: + +* 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; + +* 多线程需要协调和管理,所以需要 CPU 时间跟踪线程; + +* 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。 + + + +### 线程和进程区别 + +* 什么是线程和进程? + + * 进程 + + 一个在内存中运行的应用程序。 每个正在系统上运行的程序都是一个进程 + * 线程 + + 进程中的一个执行任务(控制单元), 它负责在程序里独立执行。 +`一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。` + +* 进程与线程的区别 + + * 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + + * 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + + * 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + + * 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的 + + * 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮。 + + * 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 + + + +### 什么是上下文切换? + +* 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +* 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 + +* 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 + +* Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + + + +### 守护线程和用户线程有什么区别呢? + +* 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 +* 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作 + + + +### 如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高? + +* windows上面用任务管理器看,linux下可以用 top 这个工具看。 + * 找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p (shift+m是找出消耗内存最高)查找出cpu利用最厉害的pid号 + * 根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查找出cpu利用率最厉害的线程号,比如top -H -p 1328 + * 将获取到的线程号转换成16进制,去百度转换一下就行 + * 使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比如jstack 31365 > /tmp/t.dat + * 编辑/tmp/t.dat文件,查找线程号对应的信息 + +`或者直接使用JDK自带的工具查看“jconsole” 、“visualVm”,这都是JDK自带的,可以直接在JDK的bin目录下找到直接使用` + + + +### 什么是线程死锁 + +* 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。 +* 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 +* 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 + +![img](https://img-blog.csdnimg.cn/20190921145226883.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNTg0MTA5,size_16,color_FFFFFF,t_70) + + + +### 形成死锁的四个必要条件是什么 + +* 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。 +* 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 +* 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 +* 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A) + + + +### 如何避免线程死锁 + +1. 避免一个线程同时获得多个锁 +2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源 +3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制 + + + +### 创建线程的四种方式 + +* 继承 Thread 类; + + ``` + public class MyThread extends Thread { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); + } + + ``` + +* 实现 Runnable 接口; + + ``` + public class MyRunnable implements Runnable { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); + } + + ``` + +* 实现 Callable 接口; + + ``` + public class MyCallable implements Callable { + @Override + public Integer call() { + System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); + return 1; + } + + ``` + +* 使用匿名内部类方式 + + ``` + public class CreateRunnable { + public static void main(String[] args) { + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 10; i++) { + System.out.println("i:" + i); + } + } + }); + thread.start(); + } + } + + ``` + + + +### 说一下 runnable 和 callable 有什么区别 + +**相同点:** + +* 都是接口 +* 都可以编写多线程程序 +* 都采用Thread.start()启动线程 + +**主要区别:** + +* Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果 +* Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。 + + + +### 线程的 run()和 start()有什么区别? + +* 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 + +* start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 + +* start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。 + +* run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。 + + + +### 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? + +这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! + +* new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到`时间片`后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 + +* 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 + +总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。 + + + +### 什么是 Callable 和 Future? + +* Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 + +* Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。 + + + +### 什么是 FutureTask + +* FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。 + + + +### 线程的状态 + +![img](http://dl.iteye.com/upload/picture/pic/116719/7e76cc17-0ad5-3ff3-954e-1f83463519d1.jpg) +* 新建(new):新创建了一个线程对象。 + +* 就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。 + +* 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; + +* 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 + + * 阻塞的情况分三种: + * (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态; + * (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态; + * (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。 +* 死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。 + + + +### Java 中用到的线程调度算法是什么? + +* 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度) + +* 有两种调度模型:分时调度模型和抢占式调度模型。 + + * 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。 + + * Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。 + + + +### 线程的调度策略 + +`线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:` + +* (1)线程体中调用了 yield 方法让出了对 cpu 的占用权利 + +* (2)线程体中调用了 sleep 方法使线程进入睡眠状态 + +* (3)线程由于 IO 操作受到阻塞 + +* (4)另外一个更高优先级线程出现 + +* (5)在支持时间片的系统中,该线程的时间片用完 + + + +### 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )? + +* 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 + +* 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。 + +* 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 + + + +### 请说出与线程同步以及线程调度相关的方法。 + +* (1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; + +* (2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常; + +* (3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关; + +* (4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; + + + +### sleep() 和 wait() 有什么区别? + +`两者都可以暂停线程的执行` + +* 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。 +* 是否释放锁:sleep() 不释放锁;wait() 释放锁。 +* 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 +* 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 + + + +### 你是如何调用 wait() 方法的?使用 if 块还是循环?为什么? + +* 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 + +* wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码: + +``` +synchronized (monitor) { + // 判断条件谓词是否得到满足 + while(!locked) { + // 等待唤醒 + monitor.wait(); + } + // 处理其他的业务逻辑 +} + +``` + + + +### 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里? + +* 因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。 + +* 有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。 + + + +### 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用? + +* 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。 + + + +### Thread 类中的 yield 方法有什么作用? + +* 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。 + +* 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了。 + + +### 为什么 Thread 类的 sleep()和 yield ()方法是静态的? + +* Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 + + + +### 线程的 sleep()方法和 yield()方法有什么区别? + +* (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; + +* (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; + +* (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; + +* (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 + + + +### 如何停止一个正在运行的线程? + +* 在java中有以下3种方法可以终止正在运行的线程: + * 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 + * 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 + * 使用interrupt方法中断线程。 + + + +### Java 中 interrupted 和 isInterrupted 方法的区别? + +* interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。 + + 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。 + +* interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。 + +* isInterrupted:是可以返回当前中断信号是true还是false,与interrupt最大的差别 + + + +### 什么是阻塞式方法? + +* 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。 + + + +### Java 中你怎样唤醒一个阻塞的线程? + +* 首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行; + +* 其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。 + + + +### notify() 和 notifyAll() 有什么区别? + +* 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 + +* notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。 + +* notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。 + + + +### 如何在两个线程间共享数据? + +* 在两个线程间共享变量即可实现共享。 + +`一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。` + + + +### Java 如何实现多线程之间的通讯和协作? + +* 可以通过中断 和 共享变量的方式实现线程间的通讯和协作 + +* 比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。 + +* Java中线程通信协作的最常见方式: + + * 一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll() + + * 二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll() + +* 线程间直接的数据交换: + + * 三.通过管道进行线程间通信:字节流、字符流 + + + +### 同步方法和同步块,哪个是更好的选择? + +* 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 + +* 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。 + +`请知道一条原则:同步的范围越小越好。` + + + +### 什么是线程同步和线程互斥,有哪几种实现方式? + +* 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。 + +* 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。 + +* 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 + +* 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 + +* 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。 + +* 实现线程同步的方法 + + * 同步代码方法:sychronized 关键字修饰的方法 + + * 同步代码块:sychronized 关键字修饰的代码块 + + * 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制 + + * 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义 + + + +### 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步? + +* 在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。 + +* 一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码 + +* 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案 + + + +### 如果你提交任务时,线程池队列已满,这时会发生什么 + +* 有俩种可能: + + (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务 + + (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy + + + +### 什么叫线程安全?servlet 是线程安全吗? + +* 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 + +* Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。 + +* Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。 + +* SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。 + +* Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。 + + + +### 在 Java 程序中怎么保证多线程的运行安全? + +* 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger + +* 方法二:使用自动锁 synchronized。 + +* 方法三:使用手动锁 Lock。 + +* 手动锁 Java 示例代码如下: + + ``` + Lock lock = new ReentrantLock(); + lock. lock(); + try { + System. out. println("获得锁"); + } catch (Exception e) { + // TODO: handle exception + } finally { + System. out. println("释放锁"); + lock. unlock(); + } + + ``` + + + +### 你对线程优先级的理解是什么? + +* 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。 + +* Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。 + +* 当然,如果你真的想设置优先级可以通过setPriority()方法设置,但是设置了不一定会该变,这个是不准确的 + + + +### 线程类的构造方法、静态块是被哪个线程调用的 + +* 这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。 + +* 如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么: + +(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的 + +(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的 + + +### Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈? + +* Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。 + +* 在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。 + +* 在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。 + + + +### 一个线程运行时发生异常会怎样? + +* 如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。 + + + +### Java 线程数过多会造成什么异常? + +* 线程的生命周期开销非常高 + +* 消耗过多的 CPU + + 资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。 + +* 降低稳定性JVM + + 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。 + + + +### 多线程的常用方法 + +| 方法 名 | 描述 | +| --------------- | ------------------------ | +| sleep() | 强迫一个线程睡眠N毫秒 | +| isAlive() | 判断一个线程是否存活。 | +| join() | 等待线程终止。 | +| activeCount() | 程序中活跃的线程数。 | +| enumerate() | 枚举程序中的线程。 | +| currentThread() | 得到当前线程。 | +| isDaemon() | 一个线程是否为守护线程。 | +| setDaemon() | 设置一个线程为守护线程。 | +| setName() | 为线程设置一个名称。 | +| wait() | 强迫一个线程等待。 | +| notify() | 通知一个线程继续运行。 | +| setPriority() | 设置一个线程的优先级。 | + + + +## 并发理论 + +### Java中垃圾回收有什么目的?什么时候进行垃圾回收? + +* 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。 + +* 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。 + + + +### 线程之间如何通信及线程之间如何同步 + +* 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。 + +* Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。 + + + +### Java内存模型 + +* 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +![img](https://upload-images.jianshu.io/upload_images/22555065-21f2dcdc26025fc2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +* 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤: + 1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 + 2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 + +**下面通过示意图来说明线程之间的通信** + +![img](https://img-blog.csdnimg.cn/20200318214515696.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzEyMjA5MA==,size_16,color_FFFFFF,t_70) + +* 总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。 + + + +### 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存? + +* 不会,在下一个垃圾回调周期中,这个对象将是被可回收的。 + +* 也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。 + + + +### finalize()方法什么时候被调用?析构函数(finalization)的目的是什么? + +* 1.垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法; finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间 + +* 1. GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。 + + * Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(File Handle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。 + + + +### 什么是重排序 + +* 程序执行的顺序按照代码的先后顺序执行。 +* 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 + +``` +int a = 5; //语句1 +int r = 3; //语句2 +a = a + 2; //语句3 +r = a*a; //语句4 + +``` + +* 则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。 +* 显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。 + + + +### 重排序实际执行的指令步骤 + +![img](https://images2018.cnblogs.com/blog/1190710/201808/1190710-20180807111124619-2023146543.png) + +1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 +2. 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 + +* 这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。 + + + +### 重排序遵守的规则 + +* as-if-serial: + 1. 不管怎么排序,结果不能改变 + 2. 不存在数据依赖的可以被编译器和处理器重排序 + 3. 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序 + 4. 单线程根据此规则不会有问题,但是重排序后多线程会有问题 + + + +### as-if-serial规则和happens-before规则的区别 + +* as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 + +* as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 + +* as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 + + + +### 并发关键字 synchronized ? + +* 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。 + +* 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + + + +### 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 + +**synchronized关键字最主要的三种使用方式:** + +* 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 +* 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 +* 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 + +`总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!` + + + +### 单例模式了解吗?给我解释一下双重检验锁方式实现单例模式! + +**双重校验锁实现对象单例(线程安全)** + +**说明:** + +* 双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载 + + ``` + public class Singleton { + private volatile static Singleton uniqueInstance; + private Singleton() {} + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } + + ``` + + } + +`另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。` + +* uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: + +1. 为 uniqueInstance 分配内存空间 +2. 初始化 uniqueInstance +3. 将 uniqueInstance 指向分配的内存地址 + +`但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。` + +`使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。` + + + +### 说一下 synchronized 底层实现原理? + +* Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成, + +* 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程: + + 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 + + 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. + + 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 + +`synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。` + + + +### synchronized可重入的原理 + +* 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。 + + + +### 什么是自旋 + +* 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。 +* 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。 + + + +### 多线程中 synchronized 锁升级的原理是什么? + +* synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。 + +`锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。` + +* 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 + +* 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁; + +* 重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。 + + + +### 线程 B 怎么知道线程 A 修改了变量 + +* (1)volatile 修饰变量 + +* (2)synchronized 修饰修改变量的方法 + +* (3)wait/notify + +* (4)while 轮询 + + + +### 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B? + +* 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。 + + + +### synchronized、volatile、CAS 比较 + +* (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。 + +* (2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。 + +* (3)CAS 是基于冲突检测的乐观锁(非阻塞) + + + +### synchronized 和 Lock 有什么区别? + +* 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类; +* synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。 +* synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 +* 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 + + + +### synchronized 和 ReentrantLock 区别是什么? + +* synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量 + +* synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。 + +* 相同点:两者都是可重入锁 + + 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 + +* 主要区别如下: + + * ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作; + * ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; + * ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。 + * 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word +* Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: + + * 普通同步方法,锁是当前实例对象 + * 静态同步方法,锁是当前类的class对象 + * 同步方法块,锁是括号里面的对象 + + + +### volatile 关键字的作用 + +* 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。 + +* 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。 + +* volatile 常用于多线程环境下的单次操作(单次读或者单次写)。 + + + +### Java 中能创建 volatile 数组吗? + +* 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。 + + + +### volatile 变量和 atomic 变量有什么不同? + +* volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。 + +* 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。 + + + +### volatile 能使得一个非原子操作变成原子操作吗? + +* 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。 + +* 虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。 + +**所以从Oracle Java Spec里面可以看到:** + +* 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。 +* 如果使用volatile修饰long和double,那么其读写都是原子操作 +* 对于64位的引用地址的读写,都是原子操作 +* 在实现JVM时,可以自由选择是否把读写long和double作为原子操作 +* 推荐JVM实现为原子操作 + + + +### synchronized 和 volatile 的区别是什么? + +* synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。 + +* volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。 + +**区别** + +* volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。 + +* volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。 + +* volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 + +* volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 + +* volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。 + + + +### final不可变对象,它对写并发应用有什么帮助? + +* 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。 + +* 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 + +* 只有满足如下状态,一个对象才是不可变的; + + * 它的状态不能在创建后再被修改; + + * 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。 + +`不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。` + + + +### Lock 接口和synchronized 对比同步它有什么优势? + +* Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。 + +* 它的优势有: + + * (1)可以使锁更公平 + + * (2)可以使线程在等待锁的时候响应中断 + + * (3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间 + + * (4)可以在不同的范围,以不同的顺序获取和释放锁 + +* 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。 + + + +### 乐观锁和悲观锁的理解及如何实现,有哪些实现方式? + +* 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 + +* 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 + + + +### 什么是 CAS + +* CAS 是 compare and swap 的缩写,即我们所说的比较交换。 + +* cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。 + +* CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。 + +`java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)` + + + +### CAS 的会产生什么问题? + +* 1、ABA 问题: + + 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 + +* 2、循环时间长开销大: + + 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。 + +* 3、只能保证一个共享变量的原子操作: + + 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。 + + + +### 什么是原子类 + +* java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。 + +* 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。 + +**简单来说就是原子类来实现CAS无锁模式的算法** + + + +### 原子类的常用类 + +* AtomicBoolean +* AtomicInteger +* AtomicLong +* AtomicReference + + + +### 说一下 Atomic的原理? + +* Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。 + + + +### 死锁与活锁的区别,死锁与饥饿的区别? + +* 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 + +* 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 + +* 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。 + +* 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。 + + Java 中导致饥饿的原因: + + * 1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。 + + * 2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 + + * 3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。 + + + +## 线程池 + +### 什么是线程池? + +* Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。 + * 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 + * 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 + * 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用 + + + +### 线程池作用? + +* 线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。 + +* 如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。 + + + +### 线程池有什么优点? + +* 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。 + +* 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 + +* 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +* 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。 + + + +### 什么是ThreadPoolExecutor? + +* **ThreadPoolExecutor就是线程池** + + ThreadPoolExecutor其实也是JAVA的一个类,我们一般通过Executors工厂类的方法,通过传入不同的参数,就可以构造出适用于不同应用场景下的ThreadPoolExecutor(线程池) + +构造参数图: + +![img](https://img-blog.csdnimg.cn/20200411195549369.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzEyMjA5MA==,size_16,color_FFFFFF,t_70) + +`构造参数参数介绍:` + +``` +corePoolSize 核心线程数量 +maximumPoolSize 最大线程数量 +keepAliveTime 线程保持时间,N个时间单位 +unit 时间单位(比如秒,分) +workQueue 阻塞队列 +threadFactory 线程工厂 +handler 线程池拒绝策略 + +``` + + + +### 什么是Executors? + +* **Executors框架实现的就是线程池的功能。** + + Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池, + +Executor工厂类如何创建线程池图: + +![img](https://img-blog.csdnimg.cn/20200411195558824.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzEyMjA5MA==,size_16,color_FFFFFF,t_70) + + + +### 线程池四种创建方式? + +* **Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:** + 1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 + 2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 + 3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 + 4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + + + +### 在 Java 中 Executor 和 Executors 的区别? + +* Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 + +* Executor 接口对象能执行我们的线程任务。 + +* ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 + +* 使用 ThreadPoolExecutor 可以创建自定义线程池。 + + + +### 四种构建线程池的区别及特点? + +**1\. newCachedThreadPool** + +* **特点**:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制 + +* **缺点**:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值 + +* **总结**:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 + +* **代码示例:** + + ```java + package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewCachedThreadPool { + public static void main(String[] args) { + // 创建无限大小线程池,由jvm自动回收 + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + for (int i = 0; i < 10; i++) { + final int temp = i; + newCachedThreadPool.execute(new Runnable() { + public void run() { + try { + Thread.sleep(100); + } catch (Exception e) { + } + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + + ``` + +**2.newFixedThreadPool** + +* **特点**:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。 + +* **缺点**:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间) + +* **总结**:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors() + +`Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量)` + +* **代码示例:** + + ``` + package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewFixedThreadPool { + public static void main(String[] args) { + ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newFixedThreadPool.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + + ``` + +**3.newScheduledThreadPool** + +* **特点**:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类) + +* **缺点**:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。 + +* **代码示例:** + + ```java + package com.lijie; + + import java.util.concurrent.Executors; + import java.util.concurrent.ScheduledExecutorService; + import java.util.concurrent.TimeUnit; + + public class TestNewScheduledThreadPool { + public static void main(String[] args) { + //定义线程池大小为3 + ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newScheduledThreadPool.schedule(new Runnable() { + public void run() { + System.out.println("i:" + temp); + } + }, 3, TimeUnit.SECONDS);//这里表示延迟3秒执行。 + } + } + } + + ``` + +**4.newSingleThreadExecutor** + +* **特点**:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +* **缺点**:缺点的话,很明显,他是单线程的,高并发业务下有点无力 + +* **总结**:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它 + +* **代码示例:** + + ``` + package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewSingleThreadExecutor { + public static void main(String[] args) { + ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); + for (int i = 0; i < 10; i++) { + final int index = i; + newSingleThreadExecutor.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + " index:" + index); + try { + Thread.sleep(200); + } catch (Exception e) { + } + } + }); + } + } + } + + ``` + + + +### 线程池都有哪些状态? + +* RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。 +* SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。 +* STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。 +* TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。 +* TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。 + + + +### 线程池中 submit() 和 execute() 方法有什么区别? + +* 相同点: + * 相同点就是都可以开启线程执行池中的任务。 +* 不同点: + * 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。 + * 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有 + * 异常处理:submit()方便Exception处理 + + + +### 什么是线程组,为什么在 Java 中不推荐使用? + +* ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。 + +* 线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。 + +* 为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。 + + + +### ThreadPoolExecutor饱和策略有哪些? + +`如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:` + +* ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。 +* ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +* ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。 +* ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。 + + + +### 如何自定义线程线程池? + +* 先看ThreadPoolExecutor(线程池)这个类的构造参数 + +![img](https://img-blog.csdnimg.cn/20200411195609334.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzEyMjA5MA==,size_16,color_FFFFFF,t_70) + +构造参数参数介绍: + + ``` + corePoolSize 核心线程数量 + maximumPoolSize 最大线程数量 + keepAliveTime 线程保持时间,N个时间单位 + unit 时间单位(比如秒,分) + workQueue 阻塞队列 + threadFactory 线程工厂 + handler 线程池拒绝策略 + + ``` + +* 代码示例: + + ```java + package com.lijie; + + import java.util.concurrent.ArrayBlockingQueue; + import java.util.concurrent.ThreadPoolExecutor; + import java.util.concurrent.TimeUnit; + + public class Test001 { + public static void main(String[] args) { + //创建线程池 + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); + for (int i = 1; i <= 6; i++) { + TaskThred t1 = new TaskThred("任务" + i); + //executor.execute(t1);是执行线程方法 + executor.execute(t1); + } + //executor.shutdown()不再接受新的任务,并且等待之前提交的任务都执行完再关闭,阻塞队列中的任务不会再执行。 + executor.shutdown(); + } + } + + class TaskThred implements Runnable { + private String taskName; + + public TaskThred(String taskName) { + this.taskName = taskName; + } + public void run() { + System.out.println(Thread.currentThread().getName() + taskName); + } + } + + ``` + + + +### 线程池的执行原理? + +![img](https://img-blog.csdnimg.cn/20200411195615530.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzEyMjA5MA==,size_16,color_FFFFFF,t_70) + +* 提交一个任务到线程池中,线程池的处理流程如下: + + 1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。 + + 2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。 + + 3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 + + + +### 如何合理分配线程池大小? + +* 要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配 + +### 什么是CPU密集 + +* CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 + +* CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。 + +### 什么是IO密集 + +* IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 + +### 分配CPU和IO密集: + +1. CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务 + +2. IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数 + +**精确来说的话的话:** + +* 从以下几个角度分析任务的特性: + + * 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。 + + * 任务的优先级:高、中、低。 + + * 任务的执行时间:长、中、短。 + + * 任务的依赖性:是否依赖其他系统资源,如数据库连接等。 + +**可以得出一个结论:** + +* 线程等待时间比CPU执行时间比例越高,需要越多线程。 +* 线程CPU执行时间比等待时间比例越高,需要越少线程。 + + + + + +## 并发容器 + +### 你经常使用什么并发容器,为什么? + +* 答:Vector、ConcurrentHashMap、HasTable + +* 一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等 + +* 但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。 + + + +### 什么是Vector + +* Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多 + + (`ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。`) + + + +### ArrayList和Vector有什么不同之处? + +* Vector方法带上了synchronized关键字,是线程同步的 + +1. ArrayList添加方法源码 + +![img](https://img-blog.csdnimg.cn/20200411195623109.jpg) + +2. Vector添加源码(加锁了synchronized关键字) + +![img](https://img-blog.csdnimg.cn/20200411195628931.jpg) + + + +### 为什么HashTable是线程安全的? + +* 因为HasTable的内部方法都被synchronized修饰了,所以是线程安全的。其他的都和HashMap一样 + +**1、HashMap添加方法的源码** + +![img](https://img-blog.csdnimg.cn/20200411195633732.jpg) + +**2、HashTable添加方法的源码** + +![img](https://img-blog.csdnimg.cn/20200411195638569.jpg) + + + + + +### 用过ConcurrentHashMap,讲一下他和HashTable的不同之处? + +* ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。 + +* 看不懂???很正常,我也看不懂 + +* **总结:** +1. HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全 + 2. ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。 + 3. 并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。 + 4. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 + 5. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容 + + + +### Collections.synchronized * 是什么? + +`注意:* 号代表后面是还有内容的` + +* 此方法是干什么的呢,他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合 + +* Collections.synchronized * :原理是什么,我猜的话是代理模式 + +**![img](https://img-blog.csdnimg.cn/20200411195644799.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzEyMjA5MA==,size_16,color_FFFFFF,t_70)** + + + + + +### Java 中 ConcurrentHashMap 的并发度是什么? + +* ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。 + +* 在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。 + + + +### 什么是并发容器的实现? + +* 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。 + +* 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。 + + + +### Java 中的同步集合与并发集合有什么区别? + +* 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 + + + +### SynchronizedMap 和 ConcurrentHashMap 有什么区别? + +* SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。 + +* ConcurrentHashMap 使用分段锁来保证在多线程下的性能。 + +* ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 + +* 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。 + +* 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。 + + + +### CopyOnWriteArrayList 是什么? + +* CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。 + +* CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。 + + + +### CopyOnWriteArrayList 的使用场景? + +* 合适读多写少的场景。 + + + +### CopyOnWriteArrayList 的缺点? + +* 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。 +* 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。 +* 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。 + + + +### CopyOnWriteArrayList 的设计思想? + +* 读写分离,读和写分开 +* 最终一致性 +* 使用另外开辟空间的思路,来解决并发冲突 + + + +## 并发队列 + +### 什么是并发队列: + +* 消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信 + +* 并发队列是什么:并发队列多个线程以有次序共享数据的重要组件 + + + +### 并发队列和并发集合的区别: + +`那就有可能要说了,我们并发集合不是也可以实现多线程之间的数据共享吗,其实也是有区别的:` + +* 队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。 + +* 并发集合就是在多个线程中共享数据的 + + + +### 怎么判断并发队列是阻塞队列还是非阻塞队列 + +* 在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。 + + + +### 阻塞队列和非阻塞队列区别 + +* 当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。 + +* 或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。 + +* 或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。 + +* 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来 + + + +### 常用并发列队的介绍: + +1. **非堵塞队列:** + + 1. **ArrayDeque, (数组双端队列)** + + ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。 + + 2. **PriorityQueue, (优先级队列)** + + PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象 + + 3. **ConcurrentLinkedQueue, (基于链表的并发队列)** + + ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。 + +2. **堵塞队列:** + + 1. **DelayQueue, (基于时间优先级的队列,延期阻塞队列)** + + DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。 + + 2. **ArrayBlockingQueue, (基于数组的并发阻塞队列)** + + ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据 + + 3. **LinkedBlockingQueue, (基于链表的FIFO阻塞队列)** + + LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。 + + 4. **LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)** + + LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 + + 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。 + + LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。 + + 5. **PriorityBlockingQueue, (带优先级的无界阻塞队列)** + + priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。 + + 6. **SynchronousQueue (并发同步阻塞队列)** + + SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。 + + 将这个类称为队列有点夸大其词。这更像是一个点。 + + + +### 并发队列的常用方法 + +`不管是那种列队,是那个类,当是他们使用的方法都是差不多的` + +| 方法名 | 描述 | +| --------------------------------- | ------------------------------------------------------------ | +| add() | 在不超出队列长度的情况下插入元素,可以立即执行,成功返回true,如果队列满了就抛出异常。 | +| offer() | 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插入指定元素,成功时返回true,如果此队列已满,则返回false。 | +| put() | 插入元素的时候,如果队列满了就进行等待,直到队列可用。 | +| take() | 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。 | +| poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。 | +| remainingCapacity() | 获取队列中剩余的空间。 | +| remove(Object o) | 从队列中移除指定的值。 | +| contains(Object o) | 判断队列中是否拥有该值。 | +| drainTo(Collection c) | 将队列中值,全部移除,并发设置到给定的集合中。 | + + + +## 并发工具类 + +### 常用的并发工具类有哪些? + +* CountDownLatch + + CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。 + +* CyclicBarrier (回环栅栏) CyclicBarrier它的作用就是会让所有线程都等待完成后才会继续下一步行动。 + + CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。 + + CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。 + +* Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。 + + \ No newline at end of file diff --git "a/interviewDoc/Java/base/Java\351\233\206\345\220\210&\345\256\271\345\231\250.md" "b/interviewDoc/Java/base/Java\351\233\206\345\220\210&\345\256\271\345\231\250.md" new file mode 100644 index 0000000..91dfd72 --- /dev/null +++ "b/interviewDoc/Java/base/Java\351\233\206\345\220\210&\345\256\271\345\231\250.md" @@ -0,0 +1,1361 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + +- [] + - [什么是集合](#什么是集合) + - [集合的特点](#集合的特点) + - [集合和数组的区别](#集合和数组的区别) + - [使用集合框架的好处](#使用集合框架的好处) + - [常用的集合类有哪些?](#常用的集合类有哪些) + - [Java中的集合及其继承关系](#java中的集合及其继承关系) + - [List,Set,Map三者的区别?](#listsetmap三者的区别) + - [List、Map、Set三个接口,存取元素时,各有什么特点?](#listmapset三个接口存取元素时各有什么特点) + - [集合框架底层数据结构](#集合框架底层数据结构) + - [哪些集合类是线程安全的?](#哪些集合类是线程安全的) + - [Java集合的快速失败机制 “fail-fast”?](#java集合的快速失败机制-fail-fast) + - [怎么确保一个集合不能被修改?](#怎么确保一个集合不能被修改) + - [Comparator和Comparable的区别?](#comparator和comparable的区别) + - [poll()方法和remove()方法区别?](#poll方法和remove方法区别) + - [Java中的同步集合与并发集合有什么区别?](#java中的同步集合与并发集合有什么区别) + - [ArrayList,Vector,LinkedList的存储性能和特性](#arraylistvectorlinkedlist的存储性能和特性) + - [ArrayList和Array有什么区别?](#arraylist和array有什么区别) + - [ArrayList和HashMap默认大小?](#arraylist和hashmap默认大小) + - [如何实现集合排序?](#如何实现集合排序) + - [我们如何对一组对象进行排序?](#我们如何对一组对象进行排序) + - [集合框架中的泛型有什么优点?](#集合框架中的泛型有什么优点) + - [Java集合框架的基础接口有哪些?](#java集合框架的基础接口有哪些) +- [Collection接口](#collection接口) + - [Collections类是什么?](#collections类是什么) + - [Collection包结构,与Collections的区别](#collection包结构与collections的区别) + - [迭代器 Iterator 是什么?](#迭代器-iterator-是什么) + - [Iterator 怎么使用?有什么特点?](#iterator-怎么使用有什么特点) + - [Iterator和ListIterator的区别是什么?](#iterator和listiterator的区别是什么) + - [Iterator和ListIterator的区别是什么?](#iterator和listiterator的区别是什么) + - [如何边遍历边移除 Collection 中的元素?](#如何边遍历边移除-collection-中的元素) + - [Iterator 和 ListIterator 有什么区别?](#iterator-和-listiterator-有什么区别) + - [遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?](#遍历一个-list-有哪些不同的方式每种方法的实现原理是什么java-中-list-遍历的最佳实践是什么) + - [说一下 ArrayList 的优缺点](#说一下-arraylist-的优缺点) + - [如何实现数组和 List 之间的转换?](#如何实现数组和-list-之间的转换) + - [ArrayList 和 LinkedList 的区别是什么?](#arraylist-和-linkedlist-的区别是什么) + - [ArrayList 和 Vector 的区别是什么?](#arraylist-和-vector-的区别是什么) + - [插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?](#插入数据时arraylistlinkedlistvector谁速度较快阐述-arraylistvectorlinkedlist-的存储性能和特性) + - [多线程场景下如何使用 ArrayList?](#多线程场景下如何使用-arraylist) + - [为什么 ArrayList 的 elementData 加上 transient 修饰?](#为什么-arraylist-的-elementdata-加上-transient-修饰) + - [List 和 Set 的区别](#list-和-set-的区别) +- [Set接口](#set接口) + - [说一下 HashSet 的实现原理?](#说一下-hashset-的实现原理) + - [HashSet如何检查重复?HashSet是如何保证数据不可重复的?](#hashset如何检查重复hashset是如何保证数据不可重复的) + - [HashSet与HashMap的区别](#hashset与hashmap的区别) +- [Map接口](#map接口) + - [什么是Hash算法](#什么是hash算法) + - [什么是链表](#什么是链表) + - [说一下HashMap的实现原理?](#说一下hashmap的实现原理) + - [如何决定选用HashMap还是TreeMap?](#如何决定选用hashmap还是treemap) + - [Map接口提供了哪些不同的集合视图?](#map接口提供了哪些不同的集合视图) + - [HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现](#hashmap在jdk17和jdk18中有哪些不同hashmap的底层实现) + - [什么是红黑树](#什么是红黑树) + - [HashMap的put方法的具体流程?](#hashmap的put方法的具体流程) + - [HashMap的扩容操作是怎么实现的?](#hashmap的扩容操作是怎么实现的) + - [HashMap是怎么解决哈希冲突的?](#hashmap是怎么解决哈希冲突的) + - [能否使用任何类作为 Map 的 key?](#能否使用任何类作为-map-的-key) + - [为什么HashMap中String、Integer这样的包装类适合作为Key?](#为什么hashmap中stringinteger这样的包装类适合作为key) + - [如果使用Object作为HashMap的Key,应该怎么办呢?](#如果使用object作为hashmap的key应该怎么办呢) + - [HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?](#hashmap为什么不直接使用hashcode处理后的哈希值直接作为table的下标) + - [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方) + - [HashMap 与 HashTable 有什么区别?](#hashmap-与-hashtable-有什么区别) + - [什么是TreeMap 简介](#什么是treemap-简介) + - [TreeMap的实现原理](#treemap的实现原理) + - [如何决定使用 HashMap 还是 TreeMap?](#如何决定使用-hashmap-还是-treemap) + - [HashMap 和 ConcurrentHashMap 的区别](#hashmap-和-concurrenthashmap-的区别) + - [ConcurrentHashMap 和 Hashtable 的区别?](#concurrenthashmap-和-hashtable-的区别) + - [1 、HashTable:](#1-hashtable) + - [2 、 JDK1.7的ConcurrentHashMap:](#2--jdk17的concurrenthashmap) + - [3 、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):](#3-jdk18的concurrenthashmaptreebin-红黑二叉树节点-node-链表节点) + - [ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?](#concurrenthashmap-底层具体实现知道吗实现原理是什么) + - [如何保证线程安全又效率高?](#如何保证线程安全又效率高) +- [辅助工具类](#辅助工具类) + - [Array 和 ArrayList 有何区别?](#array-和-arraylist-有何区别) + - [如何实现 Array 和 List 之间的转换?](#如何实现-array-和-list-之间的转换) + - [comparable 和 comparator的区别?](#comparable-和-comparator的区别) + - [Collection 和 Collections 有什么区别?](#collection-和-collections-有什么区别) + - [TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?](#treemap-和-treeset-在排序时如何比较元素collections-工具类中的-sort方法如何比较元素) + - [Collection 和 Collections 有什么区别?](#collection-和-collections-有什么区别) + - [TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?](#treemap-和-treeset-在排序时如何比较元素collections-工具类中的-sort方法如何比较元素) + + +### 什么是集合 + +* 集合就是一个放数据的容器,准确的说是放数据对象引用的容器 + +* 集合类存放的都是对象的引用,而不是对象的本身 + +* 集合类型主要有3种:set(集)、list(列表)和map(映射)。 + + + +### 集合的特点 + +* 集合的特点主要有如下两点: +* 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。 + +* 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小 + + + +### 集合和数组的区别 + +* 数组是固定长度的;集合可变长度的。 + +* 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。 + +* 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 + + + +### 使用集合框架的好处 + +1. 容量自增长; +2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量; +3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。 +4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。 + + + +### 常用的集合类有哪些? + +* Map接口和Collection接口是所有集合框架的父接口: + +1. Collection接口的子接口包括:Set接口和List接口 +2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 +3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 +4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等 + + + +### Java中的集合及其继承关系 + +关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可: + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210401132042.png) + + + + + +### List,Set,Map三者的区别? + +![](https://user-gold-cdn.xitu.io/2020/4/13/17173551e70de4bd?imageslim) +* Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 + +* Collection集合主要有List和Set两大接口 + +* List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 +* Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 +* Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 + +* Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap + + + +### List、Map、Set三个接口,存取元素时,各有什么特点? + +首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个共同的父接口,叫Collection。 + +**1、Set里面不允许有重复的元素** + +即不能有两个相等(注意,不是仅仅是相同)的对象,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,**所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。**Set取元素时,不能细说要取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。 + +**2、List表示有先后顺序的集合** + +注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obje)方法时,每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(intindex,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象。List除了可以用Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。 + +**3、Map与List和Set不同** + +它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key所对应的value。另外,也可以获得所有的key的结合,还可以获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。 + +**总结** + +List以特定次序来持有元素,可有重复元素。Set无法拥有重复元素,内部排序。Map保存key-value值,value可多值。 + + + +### 集合框架底层数据结构 + +* Collection + + 1. List + + * Arraylist: Object数组 + +* Vector: Object数组 + +* LinkedList: 双向循环链表 + +2. Set + + * HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 +* LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 +* TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) +* Map + + * HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 +* LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 +* HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 +* TreeMap: 红黑树(自平衡的排序二叉树) + + + +### 哪些集合类是线程安全的? + +* Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。 +* hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。 +* ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用) +* ... + + + +### Java集合的快速失败机制 “fail-fast”? + +* 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。 + +* 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 + +* 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 + +* 解决办法: + +1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。 + +2. 使用CopyOnWriteArrayList来替换ArrayList + + + +### 怎么确保一个集合不能被修改? + +* 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。 + +* 示例代码如下: + + ``` + List list = new ArrayList<>(); + list. add("x"); + Collection clist = Collections. unmodifiableCollection(list); + clist. add("y"); // 运行时此行报错 + System. out. println(list. size()); + + ``` + + + +### Comparator和Comparable的区别? + +**相同点** + +都是用于比较两个对象“顺序”的接口 + +都可以使用Collections.sort()方法来对对象集合进行排序 + +**不同点** + +Comparable位于java.lang包下,而Comparator则位于java.util包下 + +Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序 + +**总结** + +使用Comparable接口来实现对象之间的比较时,可以使这个类型(设为A)实现Comparable接口,并可以使用Collections.sort()方法来对A类型的List进行排序,之后可以通过a1.comparaTo(a2)来比较两个对象; + +当使用Comparator接口来实现对象之间的比较时,只需要创建一个实现Comparator接口的比较器(设为AComparator),并将其传给Collections.sort()方法即可对A类型的List进行排序,之后也可以通过调用比较器AComparator.compare(a1, a2)来比较两个对象。 + +可以说一个是自己完成比较,一个是外部程序实现比较的差别而已。 + +用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。 + +比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际上你也不能这么做)去改变它的排序行为,这时候只要(也只有)使用一个实现了 Comparator 接口的对象来实现控制它的排序就行了。 + +**两种方式,各有各的特点:** 使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,后续比较规则的修改,仅仅是改这个比较器中的代码即可。 + + + +### poll()方法和remove()方法区别? + +poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。 + + + +### Java中的同步集合与并发集合有什么区别? + +同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合`ConcurrentHashMap`,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 + +不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。 + +同步`HashMap`, `Hashtable`, `HashSet`, `Vector`, `ArrayList `相比他们并发的实现(`ConcurrentHashMap`, `CopyOnWriteArrayList`, `CopyOnWriteHashSet`)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。 + +比如`ConcurrentHashMap `会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。 + +同样的,`CopyOnWriteArrayList `允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。 + +如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。 + + + +### ArrayList,Vector,LinkedList的存储性能和特性 + +`ArrayList`和`Vector`都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢, + +`Vector`由于使用了`synchronized`方法(线程安全),通常性能上较`ArrayList`差。而`LinkedList`使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,索引就变慢了,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。 + +`LinkedList`也是线程不安全的,`LinkedList`提供了一些方法,使得`LinkedList`可以被当作堆栈和队列来使用。 + + + +### ArrayList和Array有什么区别? + +Array可以容纳基本类型和对象,而ArrayList只能容纳对象。 + +ArrayList 是Java集合框架类的一员,可以称它为一个动态数组. array 是静态的,所以一个数据一旦创建就无法更改他的大小 + + + +### ArrayList和HashMap默认大小? + +在 java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段 + +```java +private static final int DEFAULT_CAPACITY = 10; + + //from HashMap.java JDK 7 +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` + + + +### 如何实现集合排序? + +你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过 Collections.sort() 来排序。 + + + +### 我们如何对一组对象进行排序? + +如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collection.sort()方法。两个类都有用于自然排序(使用Comparable)或基于标准的排序(使用Comparator)的重载方法sort()。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。 + + + + + +### 集合框架中的泛型有什么优点? + +Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。 + +泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。 + + + +### Java集合框架的基础接口有哪些? + +Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。 + +Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。 + +List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。 + +Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。 + +一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。 + + + + + +## Collection接口 + +### Collections类是什么? + +Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。 + + + +### Collection包结构,与Collections的区别 + +Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、Set; + +Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的Collection框架。 + + + +### 迭代器 Iterator 是什么? + +* Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 +* 因为所有Collection接继承了Iterator迭代器 + +![](https://user-gold-cdn.xitu.io/2020/4/13/17173551e6f6342b?imageslim) + + + +### Iterator 怎么使用?有什么特点? + +* Iterator 使用代码如下: + + ``` + List list = new ArrayList<>(); + Iterator it = list. iterator(); + while(it. hasNext()){ + String obj = it. next(); + System. out. println(obj); + } + + ``` + +* Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。 + +### Iterator和ListIterator的区别是什么? + +下面列出了他们的区别: Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。 Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 + + + +### Iterator和ListIterator的区别是什么? + +下面列出了他们的区别: Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。 Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 + + + +### 如何边遍历边移除 Collection 中的元素? + +* 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下: + + ``` + Iterator it = list.iterator(); + while(it.hasNext()){ + *// do something* + it.remove(); + } + + ``` + +一种最常见的**错误**代码如下: + +``` +for(Integer i : list){ + list.remove(i) +} + +``` + +* 运行以上错误代码会报 **ConcurrentModificationException 异常**。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。 + + + +### Iterator 和 ListIterator 有什么区别? + +* Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 +* Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 +* ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 + + + +### 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? + +* 遍历方式有以下几种: + +1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 +2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 +3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 +* 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 + +* 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。 + +* 如果没有实现该接口,表示不支持 Random Access,如LinkedList。 + +* 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 + + + +### 说一下 ArrayList 的优缺点 + +* ArrayList的优点如下: + +* ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。 +* ArrayList 在顺序添加一个元素的时候非常方便。 +* ArrayList 的缺点如下: + +* 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。 +* 插入元素的时候,也需要做一次元素复制操作,缺点同上。 +* ArrayList 比较适合顺序添加、随机访问的场景。 + + + +### 如何实现数组和 List 之间的转换? + +* 数组转 List:使用 Arrays. asList(array) 进行转换。 +* List 转数组:使用 List 自带的 toArray() 方法。 + +* 代码示例: + + ``` + // list to array + List list = new ArrayList(); + list.add("123"); + list.add("456"); + list.toArray(); + + // array to list + String[] array = new String[]{"123","456"}; + Arrays.asList(array); + + ``` + + + +### ArrayList 和 LinkedList 的区别是什么? + +* 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 +* 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 +* 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 +* 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 +* 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; + +* 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 + +* LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 + + + +### ArrayList 和 Vector 的区别是什么? + +* 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 + +* 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。 +* 性能:ArrayList 在性能方面要优于 Vector。 +* 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 +* Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 + +* Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 + + + +### 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? + +* ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 + +* Vector 中的方法由于加了 synchronized 修饰,因此 **Vector** **是线程安全容器,但性能上较ArrayList差**。 + +* LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 **LinkedList** **插入速度较快**。 + + + +### 多线程场景下如何使用 ArrayList? + +* ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: + + ``` + List synchronizedList = Collections.synchronizedList(list); + synchronizedList.add("aaa"); + synchronizedList.add("bbb"); + + for (int i = 0; i < synchronizedList.size(); i++) { + System.out.println(synchronizedList.get(i)); + } + + ``` + + + +### 为什么 ArrayList 的 elementData 加上 transient 修饰? + +* ArrayList 中的数组定义如下: + +private transient Object[] elementData; + +* 再看一下 ArrayList 的定义: + + ``` + public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable + + ``` + +* 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: + + ``` + private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ + *// Write out element count, and any hidden stuff* + int expectedModCount = modCount; + s.defaultWriteObject(); + *// Write out array length* + s.writeInt(elementData.length); + *// Write out all elements in the proper order.* + for (int i=0; i map; + + public HashSet() { + map = new HashMap<>(); + } + + public boolean add(E e) { + // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 + return map.put(e, PRESENT)==null; + } + + ``` + +**hashCode()与equals()的相关规定**: + +1. 如果两个对象相等,则hashcode一定也是相同的 +* hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值 +2. 两个对象相等,对两个equals方法返回true +3. 两个对象有相同的hashcode值,它们也不一定是相等的 +4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 +5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 + +**==与equals的区别** + +1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 +2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 + + + +### HashSet与HashMap的区别 + +> | HashMap | HashSet | +> | ------------------------------------------------------ | ------------------------------------------------------------ | +> | 实现了Map接口 | 实现Set接口 | +> | 存储键值对 | 仅存储对象 | +> | 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 | +> | HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false | +> | HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 | + + + + + +## Map接口 + +### 什么是Hash算法 + +* 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + + + +### 什么是链表 + +* 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。 + +* 链表大致分为单链表和双向链表 + +1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针 + +![](https://user-gold-cdn.xitu.io/2020/4/13/17173551e72891e5?imageslim) + + 2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针 +![](https://user-gold-cdn.xitu.io/2020/4/13/17173551e73f80b0?imageslim) +* 链表的优点 + +* 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素) +* 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间) +* 大小没有固定,拓展很灵活。 +* 链表的缺点 + +* 不能随机查找,必须从第一个开始遍历,查找效率低 + + + +### 说一下HashMap的实现原理? + +* HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 + +* HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 + +* HashMap 基于 Hash 算法实现的 + +1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 + +2. 存储时,如果出现hash值相同的key,此时有两种情况。 + +(1)如果key相同,则覆盖原始值; + +(2)如果key不同(出现冲突),则将当前的key-value放入链表中 + +3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 + +4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 + +* 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) + + + +### 如何决定选用HashMap还是TreeMap? + +对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 + + + +### Map接口提供了哪些不同的集合视图? + +Map接口提供三个集合视图: + +**1、Set keyset():** 返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 + +**2、Collection values():** 返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 + +**3、Set> entrySet():** 返回一个map钟包含的所有映射的一个集合视图。这个集合受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 + + + +### HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。**数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;**所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做**拉链法**的方式可以解决哈希冲突。 + +**HashMap JDK1.8之前** + +* JDK1.8之前采用的是拉链法。**拉链法**:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +![](https://user-gold-cdn.xitu.io/2020/4/13/17173551e78f59a7?imageslim) + +**HashMap JDK1.8之后** + +* 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 + +![](https://user-gold-cdn.xitu.io/2020/4/13/17173551e7c6af15?imageslim) + +**JDK1.7 VS JDK1.8 比较** + +* JDK1.8主要解决或优化了一下问题: +1. resize 扩容优化 +2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 +3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 + +> | 不同 | JDK 1.7 | JDK 1.8 | +> | ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +> | 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 | +> | 初始化方式 | 单独函数:`inflateTable()` | 直接集成到了扩容函数`resize()`中 | +> | hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 | +> | 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 | +> | 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) | +> | 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) | + + + +### 什么是红黑树 + +**说道红黑树先讲什么是二叉树** + +* 二叉树简单来说就是 每一个节上可以关联俩个子节点 + +* ``` + 大概就是这样子: +a +/ \ +b c +/ \ / \ +d e f g +/ \ / \ / \ / \ +h i j k l m n o + + ``` + +**红黑树** + +* 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。![](data:image/svg+xml;utf8,) + +* 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。 + +* 如果一个结点是红色的,则它的子结点必须是黑色的。 + +* 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!] + +* 红黑树的基本操作是**添加、删除**。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。 + + + +### HashMap的put方法的具体流程? + +* 当我们put的时候,首先计算 `key`的`hash`值,这里调用了 `hash`方法,`hash`方法实际是让`key.hashCode()`与`key.hashCode()>>>16`进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:**高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞**。按照函数注释,因为bucket数组大小是2的幂,计算下标`index = (table.length - 1) & hash`,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 + +* putVal方法执行流程图 + +![](https://user-gold-cdn.xitu.io/2020/4/13/1717355218a84ee7?imageslim) + +```java +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} + +//实现Map.put和相关方法 +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 步骤①:tab为空则创建 + // table未初始化或者长度为0,进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 步骤②:计算index,并对null做处理 + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素 + else { + Node e; K k; + // 步骤③:节点key存在,直接覆盖value + // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 将第一个元素赋值给e,用e来记录 + e = p; + // 步骤④:判断该链为红黑树 + // hash值不相等,即key不相等;为红黑树结点 + // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null + else if (p instanceof TreeNode) + // 放入树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 步骤⑤:该链为链表 + // 为链表结点 + else { + // 在链表最末插入结点 + for (int binCount = 0; ; ++binCount) { + // 到达链表的尾部 + + //判断该链表尾部指针是不是空的 + if ((e = p.next) == null) { + // 在尾部插入新结点 + p.next = newNode(hash, key, value, null); + //判断链表的长度是否达到转化红黑树的临界值,临界值为8 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //链表结构转树形结构 + treeifyBin(tab, hash); + // 跳出循环 + break; + } + // 判断链表中结点的key值与插入的元素的key值是否相等 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // 相等,跳出循环 + break; + // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + p = e; + } + } + //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值 + if (e != null) { + // 记录e的value + V oldValue = e.value; + // onlyIfAbsent为false或者旧值为null + if (!onlyIfAbsent || oldValue == null) + //用新值替换旧值 + e.value = value; + // 访问后回调 + afterNodeAccess(e); + // 返回旧值 + return oldValue; + } + } + // 结构性修改 + ++modCount; + // 步骤⑥:超过最大容量就扩容 + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} + +``` + +1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; +2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; +3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; +4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5; +5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; +6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 + + + +### HashMap的扩容操作是怎么实现的? + +1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; + +2. 每次扩展的时候,都是扩展2倍; + +3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 + +* 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 + + ```java + final Node[] resize() { + Node[] oldTab = table;//oldTab指向hash桶数组 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 + if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值 + threshold = Integer.MAX_VALUE; + return oldTab;//返回 + }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold + } + // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂 + // 直接将该值赋给新的容量 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75 + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 新的threshold = 新的cap * 0.75 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + // 计算出新的数组长度后赋给当前成员变量table + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组 + table = newTab;//将新数组的值复制给旧的hash桶数组 + // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散 + if (oldTab != null) { + // 遍历新数组的所有桶下标 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收 + oldTab[j] = null; + // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 + if (e.next == null) + // 用同样的hash映射算法把该元素加入新的数组 + newTab[e.hash & (newCap - 1)] = e; + // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + // e是链表的头并且e.next!=null,那么处理链表中元素重排 + else { // preserve order + // loHead,loTail 代表扩容后不用变换下标,见注1 + Node loHead = null, loTail = null; + // hiHead,hiTail 代表扩容后变换下标,见注1 + Node hiHead = null, hiTail = null; + Node next; + // 遍历链表 + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead + // 代表下标保持不变的链表的头元素 + loHead = e; + else + // loTail.next指向当前e + loTail.next = e; + // loTail指向当前的元素e + // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时, + // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next..... + // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。 + loTail = e; + } + else { + if (hiTail == null) + // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素 + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; + } + + ``` + + + +### HashMap是怎么解决哈希冲突的? + +* 答:在解决这个问题之前,我们首先需要知道**什么是哈希冲突**,而在了解哈希冲突之前我们还要知道**什么是哈希**才行; + +**什么是哈希?** + +* Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +**什么是哈希冲突?** + +* **当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)**。 + +**HashMap的数据结构** + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 +* 数组的特点是:寻址容易,插入和删除困难; +* 链表的特点是:寻址困难,但插入和删除容易; +* 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突: + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/13/171735521c92dc84?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +* 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; +* 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 +* **但相比于hashCode返回的int类型,我们HashMap初始的容量大小`DEFAULT_INITIAL_CAPACITY = 1 << 4`(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表**,所以我们还需要对hashCode作一定的优化 + +**hash()函数** + +* 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于**参与运算的只有hashCode的低位**,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为**扰动**,在**JDK 1.8**中的hash()函数如下: + + ``` + static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) + } + + ``` + +* 这比在**JDK 1.7**中,更为简洁,**相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)**; + +**总结** + +* 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的: +* 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; +* 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 + + + +### 能否使用任何类作为 Map 的 key? + +可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: + +* 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。 + +* 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 + +* 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。 + +* 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。 + + + +### 为什么HashMap中String、Integer这样的包装类适合作为Key? + +* 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 +* 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 +* 内部已重写了`equals()`、`hashCode()`等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; + + + +### 如果使用Object作为HashMap的Key,应该怎么办呢? + +* 答:重写`hashCode()`和`equals()`方法 +1. **重写`hashCode()`是因为需要计算存储数据的存储位置**,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; +2. **重写`equals()`方法**,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,**目的是为了保证key在哈希表中的唯一性**; + + + +### HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? + +* 答:`hashCode()`方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过`hashCode()`计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; + +* **那怎么解决呢?** + + 1. HashMap自己实现了自己的`hash()`方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; + +2. 在保证数组长度为2的幂次方的时候,使用`hash()`运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题; + + + +### HashMap 的长度为什么是2的幂次方 + +* 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 + +* **这个算法应该如何设计呢?** + + * 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 +* **那为什么是两次扰动呢?** + + * 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; + + + +### HashMap 与 HashTable 有什么区别? + +1. **线程安全**: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +2. **效率**: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +3. **对Null key 和Null value的支持**: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 +4. **初始容量大小和每次扩充容量大小的不同** : +1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。 +2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 +5. **底层数据结构**: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 +6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 + + + +### 什么是TreeMap 简介 + +* TreeMap 是一个**有序的key-value集合**,它是通过红黑树实现的。 +* TreeMap基于**红黑树(Red-Black tree)实现**。该映射根据**其键的自然顺序进行排序**,或者根据**创建映射时提供的 Comparator 进行排序**,具体取决于使用的构造方法。 +* TreeMap是线程**非同步**的。 + + + +### TreeMap的实现原理 + +TreeMap是一个通过红黑树实现有序的key-value集合。 + +TreeMap继承AbstractMap,也即实现了Map,它是一个Map集合 + +TreeMap实现了NavigableMap接口,它支持一系列的导航方法, + +TreeMap实现了Cloneable接口,它可以被克隆 + +TreeMap本质是Red-Black Tree,它包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。 + + + +### 如何决定使用 HashMap 还是 TreeMap? + +* 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 + + + +### HashMap 和 ConcurrentHashMap 的区别 + +1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) +2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 + + + +### ConcurrentHashMap 和 Hashtable 的区别? + +* ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 + +* **底层数据结构**: JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; +* **实现线程安全的方式**: +1. **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; +2. ② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 +* **两者的对比图**: + +##### 1、HashTable: + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/13/171735521ca71b79?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +##### 2、 JDK1.7的ConcurrentHashMap: + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/13/171735521de4886d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +##### 3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点): + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/13/171735522b19186a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +* 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 + + + +### ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? + +**JDK1.7** + +* 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +* 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: + +* 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/13/171735524c5089b8?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色; +2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 + +**JDK1.8** + +* 在**JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现**,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 + +* 结构如下: + +![在这里插入图片描述](https://user-gold-cdn.xitu.io/2020/4/13/17173552564c22be?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +* **附加源码,有需要的可以看看** + +* 插入元素过程(建议去看看源码): + +* 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据; + + ``` + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + if (casTabAt(tab, i, null, new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } + + ``` + +* 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点; + + ``` + if (fh >= 0) { + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, value, null); + break; + } + } + } + + ``` + +1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值; +2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount; + + + +### 如何保证线程安全又效率高? + +Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。 + +ConcurrentHashMap将整个Map分为N个segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认N为16。 + + + +## 辅助工具类 + +### Array 和 ArrayList 有何区别? + +* Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。 +* Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。 +* Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 + +`对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。` + + + +### 如何实现 Array 和 List 之间的转换? + +* Array 转 List: Arrays. asList(array) ; +* List 转 Array:List 的 toArray() 方法。 + + + +### comparable 和 comparator的区别? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + + + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + + + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + + + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + + + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +* 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。 \ No newline at end of file diff --git "a/interviewDoc/Java/base/\345\255\227\347\254\246\344\270\262&\351\233\206\345\220\210.md" "b/interviewDoc/Java/base/\345\255\227\347\254\246\344\270\262&\351\233\206\345\220\210.md" new file mode 100644 index 0000000..7a53783 --- /dev/null +++ "b/interviewDoc/Java/base/\345\255\227\347\254\246\344\270\262&\351\233\206\345\220\210.md" @@ -0,0 +1,1227 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + + + +- [Java 中操作字符串都有哪些类?它们之间有什么区别?](#java-中操作字符串都有哪些类它们之间有什么区别) +- [String、StringBuffer和StringBuilder区别(类似上一题)](#stringstringbuffer和stringbuilder区别类似上一题) +- [String str="i"与 String str=new String("i")一样吗?](#string-stri与-string-strnew-stringi一样吗) +- [String 类的常用方法都有那些?](#string-类的常用方法都有那些) +- [String s = new String("xyz");创建了几个StringObject?是否可以继承String类?](#string-s--new-stringxyz创建了几个stringobject是否可以继承string类) +- [下面这条语句一共创建了多少个对象:String s="a"+"b"+"c"+"d"](#下面这条语句一共创建了多少个对象string-sabcd) +- [简述Java中的集合](#简述java中的集合) +- [List、Map、Set三个接口,存取元素时,各有什么特点?](#listmapset三个接口存取元素时各有什么特点) +- [Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?是用==还是equals()?它们有何区别?](#set里的元素是不能重复的那么用什么方法来区分重复与否呢是用还是equals它们有何区别) +- [ArrayList和LinkedList区别?](#arraylist和linkedlist区别) +- [ArrayList和Vector的区别](#arraylist和vector的区别) +- [ArrayList,Vector,LinkedList的存储性能和特性](#arraylistvectorlinkedlist的存储性能和特性) +- [HashMap和Hashtable的区别](#hashmap和hashtable的区别) +- [Java中的同步集合与并发集合有什么区别](#java中的同步集合与并发集合有什么区别) +- [Java中的集合及其继承关系](#java中的集合及其继承关系) +- [poll()方法和remove()方法区别?](#poll方法和remove方法区别) +- [LinkedHashMap和PriorityQueue的区别](#linkedhashmap和priorityqueue的区别) +- [WeakHashMap与HashMap的区别是什么?](#weakhashmap与hashmap的区别是什么) +- [ArrayList和LinkedList的区别?](#arraylist和linkedlist的区别) +- [ArrayList和Array有什么区别?](#arraylist和array有什么区别) +- [ArrayList和HashMap默认大小?](#arraylist和hashmap默认大小) +- [Comparator和Comparable的区别?](#comparator和comparable的区别) +- [如何实现集合排序?](#如何实现集合排序) +- [如何打印数组内容](#如何打印数组内容) +- [LinkedList的是单向链表还是双向?](#linkedlist的是单向链表还是双向) +- [TreeMap是实现原理](#treemap是实现原理) +- [遍历ArrayList时如何正确移除一个元素](#遍历arraylist时如何正确移除一个元素) +- [HashMap的实现原理](#hashmap的实现原理) +- [HashMap自动扩容](#hashmap自动扩容) +- [HashMap线程安全吗?](#hashmap线程安全吗) +- [HashMap总结](#hashmap总结) +- [Java集合框架是什么?说出一些集合框架的优点?](#java集合框架是什么说出一些集合框架的优点) +- [集合框架中的泛型有什么优点?](#集合框架中的泛型有什么优点) +- [Java集合框架的基础接口有哪些?](#java集合框架的基础接口有哪些) +- [为何Collection不从Cloneable和Serializable接口继承?](#为何collection不从cloneable和serializable接口继承) +- [为何Map接口不继承Collection接口?](#为何map接口不继承collection接口) +- [Iterator是什么?](#iterator是什么) +- [Iterator和ListIterator的区别是什么?](#iterator和listiterator的区别是什么) +- [Enumeration和Iterator接口的区别?](#enumeration和iterator接口的区别) +- [为何没有像Iterator.add()这样的方法,向集合中添加元素?](#为何没有像iteratoradd这样的方法向集合中添加元素) +- [为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标?](#为何迭代器没有一个方法可以直接获取下一个元素而不需要移动游标) +- [Iterater和ListIterator之间有什么区别?](#iterater和listiterator之间有什么区别) +- [遍历一个List有哪些不同的方式?](#遍历一个list有哪些不同的方式) +- [通过迭代器fail-fast属性,你明白了什么?](#通过迭代器fail-fast属性你明白了什么) +- [fail-fast与fail-safe有什么区别?](#fail-fast与fail-safe有什么区别) +- [在迭代一个集合的时候,如何避免ConcurrentModificationException?](#在迭代一个集合的时候如何避免concurrentmodificationexception) +- [为何Iterator接口没有具体的实现?](#为何iterator接口没有具体的实现) +- [UnsupportedOperationException是什么?](#unsupportedoperationexception是什么) +- [在Java中,HashMap是如何工作的?](#在java中hashmap是如何工作的) +- [hashCode()和equals()方法有何重要性?](#hashcode和equals方法有何重要性) +- [我们能否使用任何类作为Map的key?](#我们能否使用任何类作为map的key) +- [Map接口提供了哪些不同的集合视图?](#map接口提供了哪些不同的集合视图) +- [HashMap和HashTable有何不同?](#hashmap和hashtable有何不同) +- [如何决定选用HashMap还是TreeMap?](#如何决定选用hashmap还是treemap) +- [ArrayList和Vector有何异同点?](#arraylist和vector有何异同点) +- [Array和ArrayList有何区别?什么时候更适合用Array?](#array和arraylist有何区别什么时候更适合用array) +- [ArrayList和LinkedList有何区别?](#arraylist和linkedlist有何区别) +- [哪些集合类提供对元素的随机访问?](#哪些集合类提供对元素的随机访问) +- [EnumSet是什么?](#enumset是什么) +- [哪些集合类是线程安全的?](#哪些集合类是线程安全的) +- [并发集合类是什么?](#并发集合类是什么) +- [BlockingQueue是什么?](#blockingqueue是什么) +- [队列和栈是什么,列出它们的区别?](#队列和栈是什么列出它们的区别) +- [Collections类是什么?](#collections类是什么) +- [Comparable和Comparator接口是什么?](#comparable和comparator接口是什么) +- [Comparable和Comparator接口有何区别?](#comparable和comparator接口有何区别) +- [我们如何对一组对象进行排序?](#我们如何对一组对象进行排序) +- [当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?](#当一个集合被作为参数传递给一个函数时如何才可以确保函数不能修改它) +- [我们如何从给定集合那里创建一个synchronized的集合?](#我们如何从给定集合那里创建一个synchronized的集合) +- [集合框架里实现的通用算法有哪些?](#集合框架里实现的通用算法有哪些) +- [大写的O是什么?举几个例子?](#大写的o是什么举几个例子) +- [与Java集合框架相关的有哪些最好的实践?](#与java集合框架相关的有哪些最好的实践) +- [TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?](#treemap和treeset在排序时如何比较元素collections工具类中的sort方法如何比较元素) + + + +### Java 中操作字符串都有哪些类?它们之间有什么区别? + +**操作字符串的类有**:`String`、`StringBuffer`、`StringBuilder`。 + +String 和 StringBuffer、StringBuilder 的区别在于 **String 声明的是不可变的对象**,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象。 + +而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,**所以在经常改变字符串内容的情况下最好不要使用 String。** + + + +**StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的**,但 StringBuilder 的性能却高于 StringBuffer, + +所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。 + + + +### String、StringBuffer和StringBuilder区别(类似上一题) + +**1、数据可变和不可变** + +1. `String`底层使用一个不可变的字符数组`private final char value[];`所以它内容不可变。 +2. `StringBuffer`和`StringBuilder`都继承了`AbstractStringBuilder`底层使用的是可变字符数组:`char[] value;` + +**2、线程安全** + +- `StringBuilder`是线程不安全的,效率较高;而`StringBuffer`是线程安全的,效率较低。 + +通过他们的`append()`方法来看,`StringBuffer`是有同步锁,而`StringBuilder`没有: + +```java +@Override +public synchronized StringBuffer append(Object obj) { + toStringCache = null; + super.append(String.valueOf(obj)); + return this; +} +@Override +public StringBuilder append(String str) { + super.append(str); + return this; +} +``` + +**3、 相同点** + +`StringBuilder`与`StringBuffer`有公共父类`AbstractStringBuilder`。 + +最后,操作可变字符串速度:`StringBuilder > StringBuffer > String`,这个答案就显得不足为奇了。 + + + +### String str="i"与 String str=new String("i")一样吗? + +不一样,因为内存的分配方式不一样。String str="i"的方式,Java 虚拟机会将其分配到常量池中;而 String str=new String("i") 则会被分到堆内存中。 + +代码示例: + +```java +String x = "叶痕秋"; +String y = "叶痕秋"; +String z = new String("叶痕秋"); +System.out.println(x == y); // true +System.out.println(x == z); // false +``` + +String x = "叶痕秋" 的方式,Java 虚拟机会将其分配到常量池中,而常量池中没有重复的元素,比如当执行“叶痕秋”时,java虚拟机会先在常量池中检索是否已经有“叶痕秋”,如果有那么就将“叶痕秋”的地址赋给变量,如果没有就创建一个,然后在赋给变量; + +而 String z = new String(“叶痕秋”) 则会被分到堆内存中,即使内容一样还是会创建新的对象。 + + + +### String 类的常用方法都有那些? + +- indexOf():返回指定字符的索引。 +- charAt():返回指定索引处的字符。 +- replace():字符串替换。 +- trim():去除字符串两端空白。 +- split():分割字符串,返回一个分割后的字符串数组。 +- getBytes():返回字符串的 byte 类型数组。 +- length():返回字符串长度。 +- toLowerCase():将字符串转成小写字母。 +- toUpperCase():将字符串转成大写字符。 +- substring():截取字符串。 +- equals():字符串比较。 + + + +### String s = new String("xyz");创建了几个StringObject?是否可以继承String类? + +两个或一个都有可能,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个。NewString每写一遍,就创建一个新的对象,它使用常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’,那么这里就不会创建”xyz”了,直接从缓冲区拿,这时创建了一个StringObject;但如果以前没有用过"xyz",那么此时就会创建一个对象并放入缓冲区,这种情况它创建两个对象。至于String类是否继承,答案是否定的,因为String默认final修饰,是不可继承的。 + + + +### 下面这条语句一共创建了多少个对象:String s="a"+"b"+"c"+"d" + +对于如下代码: + +```java +String s1 = "a"; + +String s2 = s1 + "b"; + +String s3 = "a" + "b"; + +System.out.println(s2 == "ab"); + +System.out.println(s3 == "ab"); +``` + +第一条语句打印的结果为false,第二条语句打印的结果为true,这说明javac编译可以对字符串常量直接相加的表达式进行优化,不必要等到运行期再去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。 + +题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的代码应该只创建了一个String对象。写如下两行代码, + +```java +String s ="a" + "b" +"c" + "d"; + +System.out.println(s== "abcd"); +``` + +最终打印的结果应该为true。 + + + +### 简述Java中的集合 + +1. Collection下:List系(有序、元素允许重复)和Set系(无序、元素不重复) + + > set根据equals和hashcode判断,一个对象要存储在Set中,必须重写equals和hashCode方法 + +2. Map下:HashMap线程不同步;TreeMap线程同步 + +3. Collection系列和Map系列:Map是对Collection的补充,两个没什么关系 + + + +### List、Map、Set三个接口,存取元素时,各有什么特点? + +首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个共同的父接口,叫Collection。 + +**1、Set里面不允许有重复的元素** + +即不能有两个相等(注意,不是仅仅是相同)的对象,即假设Set集合中有了一个A对象,现在我要向Set集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,**所以,Set集合的add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。**Set取元素时,不能细说要取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。 + +**2、List表示有先后顺序的集合** + +注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obje)方法时,每次加入的对象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用add(intindex,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个索引指向了这个对象。List除了可以用Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以调用get(index i)来明确说明取第几个。 + +**3、Map与List和Set不同** + +它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应的value,即get(Object key)返回值为key所对应的value。另外,也可以获得所有的key的结合,还可以获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。 + +#### 总结 + +List以特定次序来持有元素,可有重复元素。Set无法拥有重复元素,内部排序。Map保存key-value值,value可多值。 + + + +### Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?是用==还是equals()?它们有何区别? + +Set里的元素是不能重复的,元素重复与否是使用equals()方法进行判断的。 + +==和equal区别也是考烂了的题,这里再重复说一下: + +==操作符专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用==操作符。 + +equals方法是用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。 + +比如:两条new语句创建了两个对象,然后用a/b这两个变量分别指向了其中一个对象,这是两个不同的对象,它们的首地址是不同的,即a和b中存储的数值是不相同的,所以,表达式a==b将返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。 + + + +### ArrayList和LinkedList区别? + +1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 +2. 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 +3. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。 + + + +### ArrayList和Vector的区别 + +两个类都实现了List接口(List接口继承了`Collection`接口),**他们都是有序集合**,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,并且其中的数据是允许重复的,这是与`HashSet`之类的集合的最大不同处,`HashSet`之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。 + +**ArrayList与Vector的区别主要包括两个方面:.** + +**同步性:** + +`Vector`是线程安全的,也就是说是它的方法之间是线程同步的,而`ArrayList`是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用`ArrayList`,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用`Vector`,因为不需要我们自己再去考虑和编写线程安全的代码。 + +**数据增长:** + +`ArrayList`与`Vector`都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加`ArrayList`与`Vector`的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。`Vector`默认增长为原来两倍,而`ArrayList`的增长策略在文档中没有明确规定(从源代码看到的是增长为原来的1.5倍)。`ArrayList`与`Vector`都可以设置初始的空间大小,`Vector`还可以设置增长的空间大小,而`ArrayList`没有提供设置增长空间的方法。 + +总结:即Vector增长原来的一倍,`ArrayList`增加原来的0.5倍。 + + + +### ArrayList,Vector,LinkedList的存储性能和特性 + +`ArrayList`和`Vector`都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢, + +`Vector`由于使用了`synchronized`方法(线程安全),通常性能上较`ArrayList`差。而`LinkedList`使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,索引就变慢了,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。 + +`LinkedList`也是线程不安全的,`LinkedList`提供了一些方法,使得`LinkedList`可以被当作堆栈和队列来使用。 + + + +### HashMap和Hashtable的区别 + +`HashMap`是`Hashtable`的轻量级实现(非线程安全的实现),他们都完成了Map接口, + +主要区别在于`HashMap`允许空(null)键值(key),由于非线程安全,在只有一个线程访问的情况下,效率要高于`Hashtable`。 + +`HashMap`允许将null作为一个entry的key或者value,而`Hashtable`不允许。 + +`HashMap`把`Hashtable`的`contains`方法去掉了,改成`containsvalue`和`containsKey`。因为contains方法容易让人引起误解。 + +`Hashtable`继承自`Dictionary`类,而`HashMap`是Java1.2引进的Map interface的一个实现。 + +最大的不同是,`Hashtable`的方法是`Synchronize`的,而`HashMap`不是,在多个线程访问`Hashtable`时,不需要自己为它的方法实现同步,而`HashMap`就必须为之提供同步。 + +就`HashMap`与`HashTable`主要从三方面来说。 + +- 历史原因:`Hashtable`是基于陈旧的`Dictionary`类的,`HashMap`是Java 1.2引进的Map接口的一个实现 +- 同步性: `Hashtable`是线程安全的,也就是说是同步的,而`HashMap`是线程序不安全的,不是同步的 +- 值:只有`HashMap`可以让你将空值作为一个表的条目的key或value + + + +### Java中的同步集合与并发集合有什么区别 + +同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合`ConcurrentHashMap`,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 + +不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。 + +同步`HashMap`, `Hashtable`, `HashSet`, `Vector`, `ArrayList `相比他们并发的实现(`ConcurrentHashMap`, `CopyOnWriteArrayList`, `CopyOnWriteHashSet`)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。 + +比如`ConcurrentHashMap `会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。 + +同样的,`CopyOnWriteArrayList `允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。 + +如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。 + + + +### Java中的集合及其继承关系 + +关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可: + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210401132042.png) + + + +### poll()方法和remove()方法区别? + +poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。 + + + +### LinkedHashMap和PriorityQueue的区别 + +PriorityQueue 是一个优先级队列,保证最高或者最低优先级的的元素总是在队列头部,但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。 + + + +### WeakHashMap与HashMap的区别是什么? + +WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有任何引用时,key/value 将会被回收。 + + + +### ArrayList和LinkedList的区别? + +最明显的区别是 ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。 + + + +### ArrayList和Array有什么区别? + +Array可以容纳基本类型和对象,而ArrayList只能容纳对象。 + +ArrayList 是Java集合框架类的一员,可以称它为一个动态数组. array 是静态的,所以一个数据一旦创建就无法更改他的大小 + + + +### ArrayList和HashMap默认大小? + +在 java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段 + +```java +private static final int DEFAULT_CAPACITY = 10; + + //from HashMap.java JDK 7 + static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` + + + +### Comparator和Comparable的区别? + +**相同点** + +都是用于比较两个对象“顺序”的接口 + +都可以使用Collections.sort()方法来对对象集合进行排序 + +**不同点** + +Comparable位于java.lang包下,而Comparator则位于java.util包下 + +Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序 + +**总结** + +使用Comparable接口来实现对象之间的比较时,可以使这个类型(设为A)实现Comparable接口,并可以使用Collections.sort()方法来对A类型的List进行排序,之后可以通过a1.comparaTo(a2)来比较两个对象; + +当使用Comparator接口来实现对象之间的比较时,只需要创建一个实现Comparator接口的比较器(设为AComparator),并将其传给Collections.sort()方法即可对A类型的List进行排序,之后也可以通过调用比较器AComparator.compare(a1, a2)来比较两个对象。 + +可以说一个是自己完成比较,一个是外部程序实现比较的差别而已。 + +用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。 + +比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际上你也不能这么做)去改变它的排序行为,这时候只要(也只有)使用一个实现了 Comparator 接口的对象来实现控制它的排序就行了。 + +两种方式,各有各的特点:使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,后续比较规则的修改,仅仅是改这个比较器中的代码即可。 + + + +### 如何实现集合排序? + +你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过 Collections.sort() 来排序。 + + + +### 如何打印数组内容 + +你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。由于数组没有实现 toString() 方法,所以如果将数组传递给 System.out.println() 方法,将无法打印出数组的内容,但是 Arrays.toString() 可以打印每个元素。 + + + +### LinkedList的是单向链表还是双向? + +双向循环列表,具体实现自行查阅源码. + + + +### TreeMap是实现原理 + +TreeMap是一个通过红黑树实现有序的key-value集合。 + +TreeMap继承AbstractMap,也即实现了Map,它是一个Map集合 + +TreeMap实现了NavigableMap接口,它支持一系列的导航方法, + +TreeMap实现了Cloneable接口,它可以被克隆 + +TreeMap本质是Red-Black Tree,它包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。 + + + +### 遍历ArrayList时如何正确移除一个元素 + +错误写法示例一: + +```java +public static void remove(ArrayList list) { + for (int i = 0; i < list.size(); i++) { + String s = list.get(i); + if (s.equals("bb")) { + list.remove(s); + } + } +} +``` + +错误写法示例二: + +```java +public static void remove(ArrayList list) { + for (String s : list) { + if (s.equals("bb")) { + list.remove(s); + } + } +} +``` + +要分析产生上述错误现象的原因唯有翻一翻jdk的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的: + +```java +public boolean remove(Object o) { + if (o == null) { + for (int index = 0; index < size; index++) + if (elementData[index] == null) { + fastRemove(index); + return true; + } + } else { + for (int index = 0; index < size; index++) + if (o.equals(elementData[index])) { + fastRemove(index); + return true; + } + } + return false; +} +``` + +按一般执行路径会走到else路径下最终调用faseRemove方法: + +```java +private void fastRemove(int index) { + modCount++; + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, + numMoved); + elementData[--size] = null; // Let gc do its work +} +``` + +可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第二个元素字符串bb时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也是字符串bb)至当前位置,导致下一次循环遍历时后一个字符串bb并没有遍历到,所以无法删除。 针对这种情况可以倒序删除的方式来避免: + +```java +public static void remove(ArrayList list) { + for (int i = list.size() - 1; i >= 0; i--) { + String s = list.get(i); + if (s.equals("bb")) { + list.remove(s); + } + } +} +``` + +因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。 + +而错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中): + +```java +public Iterator iterator() { + return new Itr(); +} +``` + +这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法: + +```java +public E next() { + checkForComodification(); + try { + E next = get(cursor); + lastRet = cursor++; + return next; + } catch (IndexOutOfBoundsException e) { + checkForComodification(); + throw new NoSuchElementException(); + } +} +``` + +第一行checkForComodification方法: + +```java +final void checkForComodification() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); +} +``` + +这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法把修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或foreach的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。 + +```java +public static void remove(ArrayList list) { + Iterator it = list.iterator(); + while (it.hasNext()) { + String s = it.next(); + if (s.equals("bb")) { + it.remove(); + } + } +} +``` + + + +### HashMap的实现原理 + +HashMap是基于哈希表实现的map,哈希表(也叫关联数组)一种通用的数据结构,是Java开发者常用的类,常用来存储和获取数据,功能强大使用起来也很方便,是居家旅行...不对,是Java开发需要掌握的基本技能,也是面试必考的知识点,所以,了解HashMap是很有必要的。 + +**原理** + +简单讲解下HashMap的原理:HashMap基于Hash算法,我们通过put(key,value)存储,get(key)来获取。当传入key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket里。当计算出的hash值相同时怎么办呢,我们称之为Hash冲突,HashMap的做法是用链表和红黑树存储相同hash值的value。当Hash冲突的个数比较少时,使用链表,否则使用红黑树。 + +**内部存储结构** + +HashMap类实现了Map< K, V>接口,主要包含以下几个方法: + +- V put(K key, V value) +- V get(Object key) +- V remove(Object key) +- Boolean containsKey(Object key) + +HashMap使用了一个内部类Node< K, V>来存储数据 + +> 我阅读的是Java 8的源码,在Java 8之前存储数据的内部类是Entry< K, V>,代码大体都是一样的 + +Node代码: + +``` +static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; + ... +} +``` + +可以看见Node类中除了键值对(key-value)以外,还有额外的两个数据: + +- hash : 这个是通过计算得到的散列值 +- next:指向另一个Node,这样HashMap可以像链表一样存储数据 + +因此可以知道,HashMap的结构大致如下: + +我们可以将每个横向看成一个个的桶,每个桶中存放着具有相同Hash值的Node,通过一个list来存放每个桶。 + +**内部变量** + +```java +// 默认容量大小 +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +// 最大容量 +static final int MAXIMUM_CAPACITY = 1 << 30; +// 装载因子 +static final float DEFAULT_LOAD_FACTOR = 0.75f; +// 转换为二叉树的阀值 +static final int TREEIFY_THRESHOLD = 8; +// 转换为二叉树的最低阀值 +static final int UNTREEIFY_THRESHOLD = 6; +// 二叉树最小容量 +static final int MIN_TREEIFY_CAPACITY = 64; +// 哈希表 +transient Node[] table; +// 键值对的数量 +transient int size; +// 记录HashMap结构改变次数,与HashMap的快速失败相关 +transient int modCount; +// 扩容的阈值 +int threshold; +// 装载因子 +final float loadFactor; +``` + + + +**常用方法** + +**put操作** + +put函数大致的思路为: + +1. 对key的hashCode()做hash,然后再计算index; +2. 如果没碰撞直接放到bucket里; +3. 如果碰撞了,以链表的形式存在buckets后; +4. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树; +5. 如果节点已经存在就替换old value(保证key的唯一性) +6. 如果bucket满了(超过load factor*current capacity),就要resize。 + +```java +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; // resize()是调整table数组大小的,如果table数组为空或长度为0,重新调整大小 + if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash | 这里计算出来的i值就是存放数组的位置,如果当前位置为空,则直接放入其中 + tab[i] = newNode(hash, key, value, null); + else { // hash冲突 + Node e; K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) // 如果hash相同,并且key值也相同,则找到存放位置 + e = p; + else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { // 存放到链表中 + for (int binCount = 0; ; ++binCount) { + if ((e = p.next) == null) { // 遍历链表并将值放到链表最后 + p.next = newNode(hash, key, value, null); + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二叉树 + break; + } + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // 表示对于当前key早已经存在 + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) // 如果onlyIfAbsent为false或则oldValue为空,替换原来的值 + e.value = value; + afterNodeAccess(e); + return oldValue; // 返回原来的值 + } + } + ++modCount; // HashMap结构修改次数,主要用于判断迭代器中fail-fast + if (++size > threshold) // 如果++size后的值比阀值大,则重新调整大小 + resize(); + afterNodeInsertion(evict); + return null; +} +``` + +代码也比较容易看懂,值得注意的就是 + +```java +else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); +``` + +与 + +```java +if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二叉树 +``` + +这是Java 8相对于以前版本一个比较大的改变。 + +在Java 8以前,每次产生hash冲突,就将记录追加到链表后面,然后通过遍历链表来查找。如果某个链表中记录过大,每次遍历的数据就越多,效率也就很低,复杂度为O(n); + +在Java 8中,加入了一个常量TREEIFY_THRESHOLD=8,如果某个链表中的记录大于这个常量的话,HashMap会动态的使用一个专门的treemap实现来替换掉它。这样复杂度是O(logn),比链表的O(n)会好很多。 + +对于前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。 + + + +**get操作** + +在理解了put之后,get就很简单了。大致思路如下: + +1. bucket里的第一个节点,直接命中; +2. 如果有冲突,则通过key.equals(k)去查找对应的entry +3. 若为树,则在树中通过key.equals(k)查找,O(logn); +4. 若为链表,则在链表中通过key.equals(k)查找,O(n)。 + +``` +public V get(Object key) { + Node e; + return (e = getNode(hash(key), key)) == null ? null : e.value; +} + +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) // 如果hash相同并且key值一样则返回当前node + return first; + if ((e = first.next) != null) { + if (first instanceof TreeNode) // 如果当前node为二叉树,则在二叉树中查找 + return ((TreeNode)first).getTreeNode(hash, key); + do { // 遍历链表 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; +} +``` + + + +### HashMap自动扩容 + +如果在初始化HashMap中没有指定初始容量,那么默认容量为16,但是如果后来HashMap中存放的数量超过了16,那么便会有大量的hash冲突;在HashMap中有自动扩容机制,如果当前存放的数量大于某个界限,HashMap便会调用resize()方法,扩大HashMap的容量。 + +当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。 + +HashMap的capacity必须满足是2的N次方,如果在构造函数内指定的容量n不满足,HashMap会通过下面的算法将其转换为大于n的最小的2的N次方数。 + +```java +// 减1→移位→按位或运算→加1返回 +static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; +} +``` + + + +### HashMap线程安全吗? + +HashMap是非线程安全的,如果在多线程环境下,可以使用HashTable,HashTable中所有CRUD操作都是线程同步的,同样的,线程同步的代价就是效率变低了。 + +再Java 5以后,有了一个线程安全的HashMap——ConcurrentHashMap,ConcurrentHashMap相对于HashTable来说,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,并发性的提升是显而易见。 + +**快速失败(fast-fail)** + +“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 + +在HashMap的forEach方法中有以下代码: + +```java +@Override +public void forEach(BiConsumer action) { + Node[] tab; + if (action == null) + throw new NullPointerException(); + if (size > 0 && (tab = table) != null) { + int mc = modCount; + for (int i = 0; i < tab.length; ++i) { + for (Node e = tab[i]; e != null; e = e.next) + action.accept(e.key, e.value); + } + if (modCount != mc) + throw new ConcurrentModificationException(); + } +} +``` + +在上面我们说到,modCount是记录每次HashMap结构修改。 forEach方法会在在进入for循环之前,将modCount赋值给mc,如果在for循环之后,HashMap的结构变化了,那么导致的结果就是modCount != mc,则抛出ConcurrentModificationException()异常。 + + + +### HashMap总结 + +1、什么时候会使用HashMap?他有什么特点? 是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。 + +2、你知道HashMap的工作原理吗? 通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。 + +3、你知道get和put的原理吗?equals()和hashCode()的都有什么作用? 通过对key的hashCode()进行hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点 + +4、你知道hash的实现吗?为什么要这样实现? 在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。 + +5、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办? 如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。 + +------ + +> 前段时间因为找工作的缘故背了一些关于HashMap的面试题,死记硬背,也不是很懂,最近看了源码,很多知识才变的清晰,而且看源码挺有趣的。再接再厉。 + + + +### Java集合框架是什么?说出一些集合框架的优点? + +每种编程语言中都有集合。集合框架的部分优点如下: + +**1、**使用核心集合类降低开发成本,而非实现我们自己的集合类。 + +**2、**随着使用经过严格测试的集合框架类,代码质量会得到提高。 + +**3、**通过使用JDK附带的集合类,可以降低代码维护成本。 + +**4、**复用性和可操作性。 + + + +### 集合框架中的泛型有什么优点? + +Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。 + +泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。 + + + +### Java集合框架的基础接口有哪些? + +Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。 + +Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。 + +List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。 + +Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。 + +一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。 + + + +### 为何Collection不从Cloneable和Serializable接口继承? + +克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。 + + + +### 为何Map接口不继承Collection接口? + +尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。 + +如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范。 + + + +### Iterator是什么? + +Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。 + + + +### Iterator和ListIterator的区别是什么? + +下面列出了他们的区别: Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。 Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。 + + + +### Enumeration和Iterator接口的区别? + +Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。 + + + +### 为何没有像Iterator.add()这样的方法,向集合中添加元素? + +语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add操作,它要确保迭代的顺序。 + + + +### 为何迭代器没有一个方法可以直接获取下一个元素,而不需要移动游标? + +它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这没有意义。 + + + +### Iterater和ListIterator之间有什么区别? + +1、我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。 + +2、Iterator只可以向前遍历,而LIstIterator可以双向遍历。 + +3、ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 + + + +### 遍历一个List有哪些不同的方式? + +```java +List strList = new ArrayList<>(); +//使用for-each循环 +for(String obj : strList){ + System.out.println(obj); +} +//using iterator +Iterator it = strList.iterator(); +while(it.hasNext()){ + String obj = it.next(); + System.out.println(obj); +} +``` + +使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出ConcurrentModificationException。 + + + +### 通过迭代器fail-fast属性,你明白了什么? + +每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。 + + + +### fail-fast与fail-safe有什么区别? + +Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。 + + + +### 在迭代一个集合的时候,如何避免ConcurrentModificationException? + +在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。 + + + +### 为何Iterator接口没有具体的实现? + +Iterator接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的Iterator的集合类都有它自己的Iterator实现内部类。 + +这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList迭代器是fail-fast的,而CopyOnWriteArrayList迭代器是fail-safe的。 + + + +### UnsupportedOperationException是什么? + +UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。 + + + +### 在Java中,HashMap是如何工作的? + +HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。下面的图片解释了详细内容。 + +其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是32,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。 + + + +### hashCode()和equals()方法有何重要性? + +HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则: + +(1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。 + +(2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。 + + + +### 我们能否使用任何类作为Map的key? + +我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点: + +(1)如果类重写了equals()方法,它也应该重写hashCode()方法。 + +(2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。 + +(3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。 + +(4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。 + +比如,我有一个类MyKey,在HashMap中使用它。 + +//传递给MyKey的name参数被用于equals()和hashCode()中 MyKey key = new MyKey('Pankaj'); //assume hashCode=1234 myHashMap.put(key, 'Value'); // 以下的代码会改变key的hashCode()和equals()值 key.setName('Amit'); //assume new hashCode=7890 //下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null myHashMap.get(new MyKey('Pankaj')); 那就是为何String和Integer被作为HashMap的key大量使用。 + + + +### Map接口提供了哪些不同的集合视图? + +Map接口提供三个集合视图: + +**1、Set keyset():**返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 + +**2、Collection values():**返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 + +**3、Set> entrySet():**返回一个map钟包含的所有映射的一个集合视图。这个集合受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。 + + + +### HashMap和HashTable有何不同? + +(1)HashMap允许key和value为null,而HashTable不允许。 + +(2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境,HashTable适合多线程环境。 + +(3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。 + +(4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration进行遍历,它不支持fail-fast。 + +(5)HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用CocurrentHashMap。 + + + +### 如何决定选用HashMap还是TreeMap? + +对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 + + + +### ArrayList和Vector有何异同点? + +ArrayList和Vector在很多时候都很类似。 + +**1、** 两者都是基于索引的,内部由一个数组支持。 + +**2、** 两者维护插入的顺序,我们可以根据插入顺序来获取元素。 + +**3、 **ArrayList和Vector的迭代器实现都是fail-fast的。 + +**4、** ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。 + +以下是ArrayList和Vector的不同点。 + +**1、** Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。 + +**2、** ArrayList比Vector快,它因为有同步,不会过载。 + +**3、** ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。 + + + +### Array和ArrayList有何区别?什么时候更适合用Array? + +Array可以容纳基本类型和对象,而ArrayList只能容纳对象。 + +Array是指定大小的,而ArrayList大小是固定的。 + +Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。 + +**1、** 如果列表的大小已经指定,大部分情况下是存储和遍历它们。 + +**2、** 对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢。 + +**3、** 如果你要使用多维数组,使用[][]比List>更容易。 + + + +### ArrayList和LinkedList有何区别? + +ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。 + +**1、** ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问,复杂度为O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节点相连接。所以,尽管有使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到索引的节点然后返回元素,时间复杂度为O(n),比ArrayList要慢。 + +**2、** 与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元素被插入到中间的时候,不会涉及改变数组的大小,或更新索引。 + +**3、** LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用。 + + + +### 哪些集合类提供对元素的随机访问? + +ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。 + + + +### EnumSet是什么? + +java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。它也提供了一些有用的方法,比如copyOf(Collection c)、of(E first,E…rest)和complementOf(EnumSet s)。 + + + +### 哪些集合类是线程安全的? + +Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。 + + + +### 并发集合类是什么? + +Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。 + + + +### BlockingQueue是什么? + +Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。 + +BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。 + +Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。 + + + +### 队列和栈是什么,列出它们的区别? + +栈和队列两者都被用来预存储数据。 java.util.Queue是一个接口,它的实现类在Java并发包中。队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。 + +栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。 + +Stack是一个扩展自Vector的类,而Queue是一个接口。 + + + +### Collections类是什么? + +Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。 + + + +### Comparable和Comparator接口是什么? + +如果我们想使用Array或Collection的排序方法时,需要在自定义类里实现Java提供Comparable接口。 + +Comparable接口有compareTo(T OBJ)方法,它被排序方法所使用。我们应该重写这个方法,如果“this”对象比传递的对象参数更小、相等或更大时,它返回一个负整数、0或正整数。 + +但是,在大多数实际情况下,我们想根据不同参数进行排序。 + +比如,作为一个CEO,我想对雇员基于薪资进行排序,一个HR想基于年龄对他们进行排序。这就是我们需要使用Comparator接口的情景,因为Comparable.compareTo(Object o)方法实现只能基于一个字段进行排序,我们不能根据对象排序的需要选择字段。 + +Comparator接口的compare(Object o1, Object o2)方法的实现需要传递两个对象参数,若第一个参数比第二个小,返回负整数;若第一个等于第二个,返回0;若第一个比第二个大,返回正整数。 + + + +### Comparable和Comparator接口有何区别? + +Comparable和Comparator接口被用来对对象集合或者数组进行排序。 + +Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。 + +Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集合进行排序。 + + + +### 我们如何对一组对象进行排序? + +如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象列表,我们可以使用Collection.sort()方法。两个类都有用于自然排序(使用Comparable)或基于标准的排序(使用Comparator)的重载方法sort()。Collections内部使用数组排序方法,所有它们两者都有相同的性能,只是Collections需要花时间将列表转换为数组。 + + + +### 当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它? + +在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。 + + + +### 我们如何从给定集合那里创建一个synchronized的集合? + +我们可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。 + + + +### 集合框架里实现的通用算法有哪些? + +Java集合框架提供常用的算法实现,比如排序和搜索。Collections类包含这些方法实现。大部分算法是操作List的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。 + + + +### 大写的O是什么?举几个例子? + +大写的O描述的是,就数据结构中的一系列元素而言,一个算法的性能。Collection类就是实际的数据结构,我们通常基于时间、内存和性能,使用大写的O来选择集合实现。 + +比如: 例子1:ArrayList的get(index i)是一个常量时间操作,它不依赖list中元素的数量。所以它的性能是O(1)。 + +例子2:一个对于数组或列表的线性搜索的性能是O(n),因为我们需要遍历所有的元素来查找需要的元素。 + + + +### 与Java集合框架相关的有哪些最好的实践? + +**1、** 根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。 + +**2、** 一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。 + +**3、** 基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。 + +**4、** 总是使用类型安全的泛型,避免在运行时出现ClassCastException。 + +**5、 **使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。 + +**6、** 尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。 + + + +### TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素? + +TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。 \ No newline at end of file diff --git "a/interviewDoc/Java/base/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225.md" "b/interviewDoc/Java/base/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225.md" new file mode 100644 index 0000000..ed80c1e --- /dev/null +++ "b/interviewDoc/Java/base/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225.md" @@ -0,0 +1,3893 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + + + +- [什么是数据结构?](#什么是数据结构) +- [为什么我们需要数据结构?](#为什么我们需要数据结构) +- [常见的数据结构](#常见的数据结构) +- [冒泡排序](#冒泡排序) +- [插入排序](#插入排序) +- [选择排序](#选择排序) +- [归并排序](#归并排序) +- [快速排序](#快速排序) +- [二分查找](#二分查找) +- [二分查找 II](#二分查找-ii) +- [删除排序数组中的重复项](#删除排序数组中的重复项) +- [删除排序数组中的重复项 II](#删除排序数组中的重复项-ii) +- [移除元素](#移除元素) +- [移动零](#移动零) +- [数组中重复的数字](#数组中重复的数字) +- [旋转数组](#旋转数组) +- [螺旋矩阵](#螺旋矩阵) +- [两数之和](#两数之和) +- [三数之和](#三数之和) +- [四数之和](#四数之和) +- [较小的三数之和](#较小的三数之和) +- [最接近的三数之和](#最接近的三数之和) +- [合并两个有序数组](#合并两个有序数组) +- [寻找旋转排序数组中的最小值](#寻找旋转排序数组中的最小值) +- [寻找旋转排序数组中的最小值 II](#寻找旋转排序数组中的最小值-ii) +- [除自身以外数组的乘积](#除自身以外数组的乘积) +- [无重复字符的最长子串](#无重复字符的最长子串) +- [反转字符串中的元音字母](#反转字符串中的元音字母) +- [字符串转换整数](#字符串转换整数) +- [赎金信](#赎金信) +- [两数相加](#两数相加) +- [两数相加 II](#两数相加-ii) +- [从尾到头打印链表](#从尾到头打印链表) +- [删除链表的节点](#删除链表的节点) +- [删除排序链表中的重复元素](#删除排序链表中的重复元素) +- [删除排序链表中的重复元素 II](#删除排序链表中的重复元素-ii) +- [移除链表元素](#移除链表元素) +- [两两交换链表中的节点](#两两交换链表中的节点) +- [排序链表](#排序链表) +- [反转链表](#反转链表) +- [二叉树的前序遍历](#二叉树的前序遍历) +- [二叉树的后序遍历](#二叉树的后序遍历) +- [二叉树的中序遍历](#二叉树的中序遍历) +- [最小栈](#最小栈) +- [队列的最大值](#队列的最大值) +- [冒泡排序](#冒泡排序) +- [选择排序](#选择排序) +- [插入排序](#插入排序) +- [快速排序](#快速排序) +- [堆排序](#堆排序) +- [希尔排序](#希尔排序) +- [归并排序](#归并排序) +- [计数排序](#计数排序) +- [桶排序](#桶排序) +- [基数排序](#基数排序) +- [排序算法的各自的使用场景和适用场合。](#排序算法的各自的使用场景和适用场合) + + + +### 什么是数据结构? + +简单地说,数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题时选取最合适的数据结构。 + + + +### 为什么我们需要数据结构? + +数据是计算机科学当中最关键的实体,而数据结构则可以将数据以某种组织形式存储,因此,数据结构的价值不言而喻。 + +无论你以何种方式解决何种问题,你都需要处理数据——无论是涉及员工薪水、股票价格、购物清单,还是只是简单的电话簿问题。 + +数据需要根据不同的场景,按照特定的格式进行存储。有很多数据结构能够满足以不同格式存储数据的需求。 + + + +### 常见的数据结构 + +首先列出一些最常见的数据结构,我们将逐一说明: + +- 数组 +- 栈 +- 队列 +- 链表 +- 树 +- 图 +- 字典树(这是一种高效的树形结构,但值得单独说明) +- 散列表(哈希表) + + + +**1. 数组** + +数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。下图是一个包含元素(1,2,3和4)的简单数组,数组长度为4。 + + + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4P4GWNaCsicgmuXrsHwuU15A1Jjw0Uq6zeFyIJmQhbibLtLpA2FLiau8stw/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +每个数据元素都关联一个正数值,我们称之为索引,它表明数组中每个元素所在的位置。大部分语言将初始索引定义为零。关注Java技术栈微信公众号,回复"面试"获取更多博主精心整理的面试题。 + + + +以下是数组的两种类型: + +- 一维数组(如上所示) +- 多维数组(数组的数组) + +**数组的基本操作** + +- Insert——在指定索引位置插入一个元素 +- Get——返回指定索引位置的元素 +- Delete——删除指定索引位置的元素 +- Size——得到数组所有元素的数量 + +**面试中关于数组的常见问题** + +- 寻找数组中第二小的元素 +- 找到数组中第一个不重复出现的整数 +- 合并两个有序数组 +- 重新排列数组中的正值和负值 + + + +**2. 栈** + +著名的撤销操作几乎遍布任意一个应用。但你有没有思考过它是如何工作的呢?这个问题的解决思路是按照将最后的状态排列在先的顺序,在内存中存储历史工作状态(当然,它会受限于一定的数量)。这没办法用数组实现。但有了栈,这就变得非常方便了。 + +可以把栈想象成一列垂直堆放的书。为了拿到中间的书,你需要移除放置在这上面的所有书。这就是LIFO(后进先出)的工作原理。 + +下图是包含三个数据元素(1,2和3)的栈,其中顶部的3将被最先移除: + + + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4PCRIz8OZwFDmE7WJsroC7nXuibP7VecMdjxOI5zfH1ibxYLORLd6y2I1Q/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +**栈的基本操作** + +- Push——在顶部插入一个元素 + +- Pop——返回并移除栈顶元素 + +- isEmpty——如果栈为空,则返回true + +- Top——返回顶部元素,但并不移除它 + + + +**面试中关于栈的常见问题** + +- 使用栈计算后缀表达式 +- 对栈的元素进行排序 +- 判断表达式是否括号平衡 + + + +**3. 队列** + +与栈相似,队列是另一种顺序存储元素的线性数据结构。栈与队列的最大差别在于栈是LIFO(后进先出),而队列是FIFO,即先进先出。 + +一个完美的队列现实例子:售票亭排队队伍。如果有新人加入,他需要到队尾去排队,而非队首——排在前面的人会先拿到票,然后离开队伍。 + +下图是包含四个元素(1,2,3和4)的队列,其中在顶部的1将被最先移除: + + + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4PwRdy576qmp7gNDsSV80gvtiadBficmhMQpPiahsicX4Kh1ic9oRMebX17xg/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +移除先入队的元素、插入新元素 + +**队列的基本操作** + +- Enqueue() —— 在队列尾部插入元素 + +- Dequeue() ——移除队列头部的元素 + +- isEmpty()——如果队列为空,则返回true + +- Top() ——返回队列的第一个元素 + + + +**面试中关于队列的常见问题** + +- 使用队列表示栈 +- 对队列的前k个元素倒序 +- 使用队列生成从1到n的二进制数 + + + +**4. 链表** + +链表是另一个重要的线性数据结构,乍一看可能有点像数组,但在内存分配、内部结构以及数据插入和删除的基本操作方面均有所不同。关注Java技术栈微信公众号,回复"面试"获取更多博主精心整理的面试题。 + +链表就像一个节点链,其中每个节点包含着数据和指向后续节点的指针。 链表还包含一个头指针,它指向链表的第一个元素,但当列表为空时,它指向null或无具体内容。 + +链表一般用于实现文件系统、哈希表和邻接表。 + +这是链表内部结构的展示: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4PRcEQIvoPUx6NS8ZTEbTujKlC4jt3oCChugrD6gbF7cnYH0UsWM6xVw/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +链表包括以下类型: + +- 单链表(单向) + +- 双向链表(双向) + + + +**链表的基本操作:** + +- InsertAtEnd - 在链表的末尾插入指定元素 +- InsertAtHead - 在链接列表的开头/头部插入指定元素 +- Delete  - 从链接列表中删除指定元素 +- DeleteAtHead - 删除链接列表的第一个元素 +- Search  - 从链表中返回指定元素 +- isEmpty - 如果链表为空,则返回true + +**面试中关于链表的常见问题** + +- 反转链表 +- 检测链表中的循环 +- 返回链表倒数第N个节点 +- 删除链表中的重复项 + + + +5. 图 + +图是一组以网络形式相互连接的节点。节点也称为顶点。 一对节点(x,y)称为边(edge),表示顶点x连接到顶点y。边可以包含权重/成本,显示从顶点x到y所需的成本。 + + + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4PJNawY7TMR44xWweXqOe2JQciaJJ0j4U8DHEJ4g229dF9EgzEVqqeYibw/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +**图的类型** + +- 无向图 +- 有向图 + + + +**在程序语言中,图可以用两种形式表示:** + +- 邻接矩阵 +- 邻接表 + + + +**常见图遍历算法** + +- 广度优先搜索 +- 深度优先搜索 + + + +**面试中关于图的常见问题** + +- 实现广度和深度优先搜索 +- 检查图是否为树 +- 计算图的边数 +- 找到两个顶点之间的最短路径 + + + +**6. 树** + +树形结构是一种层级式的数据结构,由顶点(节点)和连接它们的边组成。 树类似于图,但区分树和图的重要特征是树中不存在环路。 + +树形结构被广泛应用于人工智能和复杂算法,它可以提供解决问题的有效存储机制。 + +这是一个简单树的示意图,以及树数据结构中使用的基本术语: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4Pvr8sj9KVLlf18dpIlLaYFuaUGic6AiaqpuvCn5CwRBRoD3SDYoPjxh9w/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +Root - 根节点 + +Parent - 父节点 + +Child - 子节点 + +Leaf - 叶子节点 + +Sibling - 兄弟节点 + + + +**以下是树形结构的主要类型:** + +- N元树 +- 平衡树 +- 二叉树 +- 二叉搜索树 +- AVL树 +- 红黑树 +- 2-3树 + + + +其中,二叉树和二叉搜索树是最常用的树。 + +**面试中关于树结构的常见问题:** + +- 求二叉树的高度 +- 在二叉搜索树中查找第k个最大值 +- 查找与根节点距离k的节点 +- 在二叉树中查找给定节点的祖先节点 + + + +**7. 字典树(Trie)** + +字典树,也称为“前缀树”,是一种特殊的树状数据结构,对于解决字符串相关问题非常有效。它能够提供快速检索,主要用于搜索字典中的单词,在搜索引擎中自动提供建议,甚至被用于IP的路由。 + +以下是在字典树中存储三个单词“top”,“so”和“their”的例子: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4PiaHw1lQibQLYvjpicUiafrUGIibJ9XEvices0FN05LHFNdCS4DdcJJeTzAmg/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +这些单词以顶部到底部的方式存储,其中绿色节点“p”,“s”和“r”分别表示“top”,“thus”和“theirs”的底部。 + +**面试中关于字典树的常见问题** + +- 计算字典树中的总单词数 +- 打印存储在字典树中的所有单词 +- 使用字典树对数组的元素进行排序 +- 使用字典树从字典中形成单词 +- 构建T9字典(字典树+ DFS ) + + + +**8. 哈希表** + +哈希法(Hashing)是一个用于唯一标识对象并将每个对象存储在一些预先计算的唯一索引(称为“键(key)”)中的过程。因此,对象以键值对的形式存储,这些键值对的集合被称为“字典”。可以使用键搜索每个对象。基于哈希法有很多不同的数据结构,但最常用的数据结构是哈希表。 + +哈希表通常使用数组实现。 + +**散列数据结构的性能取决于以下三个因素:** + +- 哈希函数 +- 哈希表的大小 +- 碰撞处理方法 + + + +下图为如何在数组中映射哈希键值对的说明。该数组的索引是通过哈希函数计算的。 + + + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/wc7YNPm3YxW2riac4vp8oY2Rictkibqks4PYOOOQcpeKRcP8dEKuibk5o5o14nD3NDsAOt1KD6st0ChYuIpjXoWjWQ/640?tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +**面试中关于哈希结构的常见问题:** + +- 在数组中查找对称键值对 +- 追踪遍历的完整路径 +- 查找数组是否是另一个数组的子集 +- 检查给定的数组是否不相交 + + + + + +### 冒泡排序 + +定义一个布尔变量 `hasChange`,用来标记每轮是否进行了交换。在每轮遍历开始时,将 `hasChange` 设置为 false。 + +若当轮没有发生交换,说明此时数组已经按照升序排列,`hashChange` 依然是为 false。此时外层循环直接退出,排序结束。 + + + +**代码示例** + +```java +import java.util.Arrays; + +public class BubbleSort { + + private static void bubbleSort(int[] nums) { + boolean hasChange = true; + for (int i = 0, n = nums.length; i < n - 1 && hasChange; ++i) { + hasChange = false; + for (int j = 0; j < n - i - 1; ++j) { + if (nums[j] > nums[j + 1]) { + swap(nums, j, j + 1); + hasChange = true; + } + } + } + } + + private static void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } + + public static void main(String[] args) { + int[] nums = {1, 2, 7, 9, 5, 8}; + bubbleSort(nums); + System.out.println(Arrays.toString(nums)); + } +} +``` + +**算法分析** + +空间复杂度 O(1)、时间复杂度 O(n²)。 + +**分情况讨论:** + +1. 给定的数组按照顺序已经排好:只需要进行 `n-1` 次比较,两两交换次数为 0,时间复杂度为 O(n),这是最好的情况。 +2. 给定的数组按照逆序排列:需要进行 `n*(n-1)/2` 次比较,时间复杂度为 O(n²),这是最坏的情况。 +3. 给定的数组杂乱无章。在这种情况下,平均时间复杂度 O(n²)。 + +因此,时间复杂度是 O(n²),这是一种稳定的排序算法。 + +> 稳定是指,两个相等的数,在排序过后,相对位置保持不变。 + + + +### 插入排序 + +先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。 + +这是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过这种方法保持集合中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面讲的插入方法,来进行排序,于是就有了插入排序算法。 + +那么插入排序具体是如何借助上面的思想来实现排序的呢? + +首先,我们将数组中的数据分为两个区间,**已排序区间**和**未排序区间**。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。 + +与冒泡排序对比: + +- 在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的。 +- 在插入排序中,经过每一轮的排序处理后,数组前端的数是排好序的。 + +**代码示例** + +```java +import java.util.Arrays; + +public class InsertionSort { + + private static void insertionSort(int[] nums) { + for (int i = 1, j, n = nums.length; i < n; ++i) { + int num = nums[i]; + for (j = i - 1; j >=0 && nums[j] > num; --j) { + nums[j + 1] = nums[j]; + } + nums[j + 1] = num; + } + } + + public static void main(String[] args) { + int[] nums = {1, 2, 7, 9, 5, 8}; + insertionSort(nums); + System.out.println(Arrays.toString(nums)); + } +} +``` + +**算法分析** + +空间复杂度 O(1),时间复杂度 O(n²)。 + +分情况讨论: + +1. 给定的数组按照顺序排好序:只需要进行 n-1 次比较,两两交换次数为 0,时间复杂度为 O(n),这是最好的情况。 +2. 给定的数组按照逆序排列:需要进行 `n*(n-1)/2` 次比较,时间复杂度为 O(n²),这是最坏的情况。 +3. 给定的数组杂乱无章:在这种情况下,平均时间复杂度是 O(n²)。 + +因此,时间复杂度是 O(n²),这也是一种稳定的排序算法。 + + + +### 选择排序 + +选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。 + +**代码示例** + +```java +import java.util.Arrays; + +public class SelectionSort { + + private static void selectionSort(int[] nums) { + for (int i = 0, n = nums.length; i < n - 1; ++i) { + int minIndex = i; + for (int j = i; j < n; ++j) { + if (nums[j] < nums[minIndex]) { + minIndex = j; + } + } + swap(nums, minIndex, i); + } + } + + private static void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } + + public static void main(String[] args) { + int[] nums = {1, 2, 7, 9, 5, 8}; + selectionSort(nums); + System.out.println(Arrays.toString(nums)); + } +} +``` + +**算法分析** + +空间复杂度 O(1),时间复杂度 O(n²)。 + +那选择排序是稳定的排序算法吗? + +答案是否定的,**选择排序是一种不稳定的排序算法**。选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。 + +比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。正是因此,相对于冒泡排序和插入排序,选择排序就稍微逊色了。 + + + +### 归并排序 + +归并排序的核心思想是分治,把一个复杂问题拆分成若干个子问题来求解。 + +归并排序的算法思想是:把数组从中间划分为两个子数组,一直递归地把子数组划分成更小的数组,直到子数组里面只有一个元素的时候开始排序。排序的方法就是按照大小顺序合并两个元素。接着依次按照递归的顺序返回,不断合并排好序的数组,直到把整个数组排好序。 + +**代码示例** + +```java +import java.util.Arrays; + +public class MergeSort { + + private static void merge(int[] nums, int low, int mid, int high, int[] temp) { + int i = low, j = mid + 1, k = low; + while (k <= high) { + if (i > mid) { + temp[k++] = nums[j++]; + } else if (j > high) { + temp[k++] = nums[i++]; + } else if (nums[i] <= nums[j]) { + temp[k++] = nums[i++]; + } else { + temp[k++] = nums[j++]; + } + } + + System.arraycopy(tmp, low, nums, low, high - low + 1); + } + + private static void mergeSort(int[] nums, int low, int high, int[] temp) { + if (low >= high) { + return; + } + int mid = (low + high) >>> 1; + mergeSort(nums, low, mid, temp); + mergeSort(nums, mid + 1, high, temp); + merge(nums, low, mid, high, temp); + } + + private static void mergeSort(int[] nums) { + int n = nums.length; + int[] temp = new int[n]; + mergeSort(nums, 0, n - 1, temp); + } + + public static void main(String[] args) { + int[] nums = {1, 2, 7, 4, 5, 3}; + mergeSort(nums); + System.out.println(Arrays.toString(nums)); + } +} +``` + +**算法分析** + +空间复杂度 O(n),时间复杂度 O(nlogn)。 + +对于规模为 n 的问题,一共要进行 log(n) 次的切分,每一层的合并复杂度都是 O(n),所以整体时间复杂度为 O(nlogn)。 + +由于合并 n 个元素需要分配一个大小为 n 的额外数组,所以空间复杂度为 O(n)。 + +这是一种稳定的排序算法。 + + + +### 快速排序 + +快速排序也采用了分治的思想:把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。 + +**代码示例** + +```java +import java.util.Arrays; + +public class QuickSort { + + private static void quickSort(int[] nums) { + quickSort(nums, 0, nums.length - 1); + } + + private static void quickSort(int[] nums, int low, int high) { + if (low >= high) { + return; + } + int[] p = partition(nums, low, high); + quickSort(nums, low, p[0] - 1); + quickSort(nums, p[0] + 1, high); + } + + private static int[] partition(int[] nums, int low, int high) { + int less = low - 1, more = high; + while (low < more) { + if (nums[low] < nums[high]) { + swap(nums, ++less, low++); + } else if (nums[low] > nums[high]) { + swap(nums, --more, low); + } else { + ++low; + } + } + swap(nums, more, high); + return new int[] {less + 1, more}; + } + + private static void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } + + public static void main(String[] args) { + int[] nums = {1, 2, 7, 4, 5, 3}; + quickSort(nums); + System.out.println(Arrays.toString(nums)); + } +} +``` + +**算法分析** + +空间复杂度 O(logn),时间复杂度 O(nlogn)。 + +对于规模为 n 的问题,一共要进行 log(n) 次的切分,和基准值进行 n-1 次比较,n-1 次比较的时间复杂度是 O(n),所以快速排序的时间复杂度为 O(nlogn)。 + +但是,如果每次在选择基准值的时候,都不幸地选择了子数组里的最大或最小值。即每次把把数组分成了两个更小长度的数组,其中一个长度为 1,另一个的长度是子数组的长度减 1。这样的算法复杂度变成 O(n²)。 + +和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成操作来实现对数组的修改;而递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数。 + + + +**如何优化快速排序?** + +前面讲到,最坏情况下快速排序的时间复杂度是 O(n²),实际上,这种 O(n²) 时间复杂度出现的主要原因还是因为我们基准值选得不够合理。最理想的基准点是:**被基准点分开的两个子数组中,数据的数量差不多**。 + +如果很粗暴地直接选择第一个或者最后一个数据作为基准值,不考虑数据的特点,肯定会出现之前讲的那样,在某些情况下,排序的最坏情况时间复杂度是 O(n²)。 + +有两个比较常用的分区算法。 + +1. 三数取中法 + +我们从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。这样每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯取某一个数据更好。但是,如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。 + +2. 随机法 + +随机法就是每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均情况下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的 O(n²) 的情况,出现的可能性不大。 + + + +### 二分查找 + +二分查找是一种非常高效的查找算法,高效到什么程度呢?我们来分析一下它的时间复杂度。 + +假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。 + +被查找区间的大小变化为: + +``` +n, n/2, n/4, n/8, ..., n/(2^k) +``` + +可以看出来,这是一个等比数列。其中 `n/(2^k)=1` 时,k 的值就是总共缩小的次数。而每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。通过 `n/(2^k)=1`,我们可以求得 `k=log2n`,所以时间复杂度就是 O(logn)。 + +**代码示例** + +注意容易出错的 3 个地方。 + +1. 循环退出条件是 `low <= high`,而不是 `low < high`; +2. mid 的取值,可以是 `mid = (low + high) / 2`,但是如果 low 和 high 比较大的话,`low + high` 可能会溢出,所以这里写为 `mid = (low + high) >>> 1`; +3. low 和 high 的更新分别为 `low = mid + 1`、`high = mid - 1`。 + + + +**非递归实现:** + +```java +public class BinarySearch { + + private static int search(int[] nums, int low, int high, int val) { + while (low <= high) { + int mid = (low + high) >>> 1; + if (nums[mid] == val) { + return mid; + } else if (nums[mid] < val) { + low = mid + 1; + } else { + high = mid - 1; + } + } + return -1; + } + + /** + * 二分查找(非递归) + * + * @param nums 有序数组 + * @param val 要查找的值 + * @return 要查找的值在数组中的索引位置 + */ + private static int search(int[] nums, int val) { + return search(nums, 0, nums.length - 1, val); + } + + public static void main(String[] args) { + int[] nums = {1, 2, 5, 7, 8, 9}; + + // 非递归查找 + int r1 = search(nums, 7); + System.out.println(r1); + } +} +``` + +**递归实现:** + +```java +public class BinarySearch { + + private static int searchRecursive(int[] nums, int low, int high, int val) { + while (low <= high) { + int mid = (low + high) >>> 1; + if (nums[mid] == val) { + return mid; + } else if (nums[mid] < val) { + return searchRecursive(nums, mid + 1, high, val); + } else { + return searchRecursive(nums, low, mid - 1, val); + } + } + return -1; + } + + /** + * 二分查找(递归) + * + * @param nums 有序数组 + * @param val 要查找的值 + * @return 要查找的值在数组中的索引位置 + */ + private static int searchRecursive(int[] nums, int val) { + return searchRecursive(nums, 0, nums.length - 1, val); + } + + public static void main(String[] args) { + int[] nums = {1, 2, 5, 7, 8, 9}; + + // 递归查找 + int r2 = searchRecursive(nums, 7); + System.out.println(r2); + } +} +``` + + + +### 二分查找 II + +前面讲的二分查找算法,是最为简单的一种,在不存在重复元素的有序数组中,查找值等于给定值的元素。 + +接下来,我们来看看二分查找算法四种常见的变形问题,分别是: + +1. 查找第一个值等于给定值的元素 +2. 查找最后一个值等于给定值的元素 +3. 查找第一个大于等于给定值的元素 +4. 查找最后一个小于等于给定值的元素 + + + +**1、查找第一个值等于给定值的元素** + +```java +public static int search(int[] nums, int val) { + int n = nums.length; + int low = 0, high = n - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + if (nums[mid] < val) { + low = mid + 1; + } else if (nums[mid] > val) { + high = mid - 1; + } else { + // 如果nums[mid]是第一个元素,或者nums[mid-1]不等于val + // 说明nums[mid]就是第一个值为给定值的元素 + if (mid == 0 || nums[mid - 1] != val) { + return mid; + } + high = mid - 1; + } + } + return -1; +} +``` + + + +**2、查找最后一个值等于给定值的元素** + +```java +public static int search(int[] nums, int val) { + int n = nums.length; + int low = 0, high = n - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + if (nums[mid] < val) { + low = mid + 1; + } else if (nums[mid] > val) { + high = mid - 1; + } else { + // 如果nums[mid]是最后一个元素,或者nums[mid+1]不等于val + // 说明nums[mid]就是最后一个值为给定值的元素 + if (mid == n - 1 || nums[mid + 1] != val) { + return mid; + } + low = mid + 1; + } + } + return -1; +} +``` + + + +**3、查找第一个大于等于给定值的元素** + +```java +public static int search(int[] nums, int val) { + int low = 0, high = nums.length - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + if (nums[mid] < val) { + low = mid + 1; + } else { + // 如果nums[mid]是第一个元素,或者nums[mid-1]小于val + // 说明nums[mid]就是第一个大于等于给定值的元素 + if (mid == 0 || nums[mid - 1] < val) { + return mid; + } + high = mid - 1; + } + } + return -1; +} +``` + + + +**4、查找最后一个小于等于给定值的元素** + +```java +public static int search(int[] nums, int val) { + int n = nums.length; + int low = 0, high = n - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + if (nums[mid] > val) { + high = mid - 1; + } else { + // 如果nums[mid]是最后一个元素,或者nums[mid+1]大于val + // 说明nums[mid]就是最后一个小于等于给定值的元素 + if (mid == n - 1 || nums[mid + 1] > val) { + return mid; + } + low = mid + 1; + } + } + return -1; +} +``` + + + +### 删除排序数组中的重复项 + +**题目描述** + +给定一个排序数组,你需要在**原地**删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须在 **原地修改输入数组** 并在使用 O(1) 额外空间的条件下完成。 + + + +**示例 1:** + +``` +给定数组 nums = [1,1,2], + +函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + +**示例 2:** + +``` +给定 nums = [0,0,1,1,1,2,2,3,3,4], + +函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + + + +**说明:** + +为什么返回数值是整数,但输出的答案是数组呢? + +请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。 + +```java +class Solution { + public int removeDuplicates(int[] nums) { + int cnt = 0, n = nums.length; + for (int i = 1; i < n; ++i) { + if (nums[i] == nums[i - 1]) ++cnt; + else nums[i - cnt] = nums[i]; + } + return n - cnt; + } +} +``` + + + + + +### 删除排序数组中的重复项 II + +给定一个排序数组,你需要在**原地**删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须在**原地修改输入数组**并在使用 O(1) 额外空间的条件下完成。 + +**示例 1:** + +``` +给定 nums = [1,1,1,2,2,3], + +函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + +**示例 2:** + +``` +给定 nums = [0,0,1,1,1,1,2,3,3], + +函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + +**说明:** + +为什么返回数值是整数,但输出的答案是数组呢? + +请注意,输入数组是以**“引用”**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。 + +你可以想象内部操作如下: + +``` +// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝 +int len = removeDuplicates(nums); + +// 在函数里修改输入数组对于调用者是可见的。 +// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 +for (int i = 0; i < len; i++) { + print(nums[i]); +} +``` + + + +**解法** + +从数组下标 1 开始遍历数组。 + +用计数器 `cnt` 记录当前数字重复出现的次数,`cnt` 的最小计数为 0;用 `cur` 记录新数组下个待覆盖的元素位置。 + +遍历时,若当前元素 `nums[i]` 与上个元素 `nums[i-1]` 相同,则计数器 +1,否则计数器重置为 0。如果计数器小于 2,说明当前元素 `nums[i]` 可以添加到新数组中,即:`nums[cur] = nums[i]`,同时 `cur++`。 + +遍历结果,返回 `cur` 值即可。 + +```java +class Solution { + public int removeDuplicates(int[] nums) { + int cnt = 0, cur = 1; + for (int i = 1; i < nums.length; ++i) { + if (nums[i] == nums[i - 1]) ++cnt; + else cnt = 0; + if (cnt < 2) nums[cur++] = nums[i]; + } + return cur; + } +} +``` + + + +### 移除元素 + +**题目描述** + +给你一个数组 *nums* 和一个值 *val*,你需要 **[原地](https://baike.baidu.com/item/原地算法)** 移除所有数值等于 *val* 的元素,并返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 **[原地 ](https://baike.baidu.com/item/原地算法)修改输入数组**。 + +元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 + + + +**示例 1:** + +``` +给定 nums = [3,2,2,3], val = 3, + +函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + +**示例 2:** + +``` +给定 nums = [0,1,2,2,3,0,4,2], val = 2, + +函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 + +注意这五个元素可为任意顺序。 + +你不需要考虑数组中超出新长度后面的元素。 +``` + + + +**说明:** + +为什么返回数值是整数,但输出的答案是数组呢? + +请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。 + +你可以想象内部操作如下: + +``` +// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 +int len = removeElement(nums, val); + +// 在函数里修改输入数组对于调用者是可见的。 +// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。 +for (int i = 0; i < len; i++) { + print(nums[i]); +} +``` + + + +**解法** + +```java +class Solution { + public int removeElement(int[] nums, int val) { + int cnt = 0, n = nums.length; + for (int i = 0; i < n; ++i) { + if (nums[i] == val) { + ++cnt; + } else { + nums[i - cnt] = nums[i]; + } + } + return n - cnt; + } +} +``` + + + + + +### 移动零 + +**题目描述** + +给定一个数组 `nums`,编写一个函数将所有 `0` 移动到数组的末尾,同时保持非零元素的相对顺序。 + +**示例:** + +``` +[0,1,0,3,12] +``` + +**说明**: + +1. 必须在原数组上操作,不能拷贝额外的数组。 +2. 尽量减少操作次数。 + +**解法** + +```java +class Solution { + public void moveZeroes(int[] nums) { + int n; + if (nums == null || (n = nums.length) < 1) { + return; + } + int zeroCount = 0; + for (int i = 0; i < n; ++i) { + if (nums[i] == 0) { + ++zeroCount; + } else { + nums[i - zeroCount] = nums[i]; + } + } + while (zeroCount > 0) { + nums[n - zeroCount--] = 0; + } + } +} +``` + + + +### 数组中重复的数字 + +**题目描述** + +找出数组中重复的数字。 + +在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。 + +**示例 1:** + +``` +输入: +[2, 3, 1, 0, 2, 5, 3] +输出:2 或 3 +``` + +**限制:** + +``` +2 <= n <= 100000 +``` + +**解法** + +0 ~ n-1 范围内的数,分别还原到对应的位置上,如:数字 2 交换到下标为 2 的位置。 + +若交换过程中发现重复,则直接返回。 + +```java +class Solution { + public int findRepeatNumber(int[] nums) { + for (int i = 0, n = nums.length; i < n; ++i) { + while (nums[i] != i) { + if (nums[i] == nums[nums[i]]) return nums[i]; + swap(nums, i, nums[i]); + } + } + return -1; + } + + private void swap(int[] nums, int i, int j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + } +} +``` + + + +### 旋转数组 + +**题目描述** + +给定一个数组,将数组中的元素向右移动 *k* 个位置,其中 *k* 是非负数。 + +**示例 1:** + +``` +[1,2,3,4,5,6,7] +``` + +**示例 2:** + +``` +[-1,-100,3,99] +``` + +**说明:** + +- 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。 +- 要求使用空间复杂度为 O(1) 的 **原地** 算法。 + +**解法** + +若 `k=3`,`nums=[1,2,3,4,5,6,7]`。 + +先将 `nums` 整体翻转:`[1,2,3,4,5,6,7]` -> `[7,6,5,4,3,2,1]` + +再翻转 `0~k-1` 范围内的元素:`[7,6,5,4,3,2,1]` -> `[5,6,7,4,3,2,1]` + +最后翻转 `k~n-1` 范围内的元素,即可得到最终结果:`[5,6,7,4,3,2,1]` -> `[5,6,7,1,2,3,4]` + +```java +class Solution { + public void rotate(int[] nums, int k) { + if (nums == null) { + return; + } + int n = nums.length; + k %= n; + if (n < 2 || k == 0) { + return; + } + + rotate(nums, 0, n - 1); + rotate(nums, 0, k - 1); + rotate(nums, k, n - 1); + } + + private void rotate(int[] nums, int i, int j) { + while (i < j) { + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; + ++i; + --j; + } + } +} +``` + + + +### 螺旋矩阵 + +**题目描述** + +给定一个包含 *m* x *n* 个元素的矩阵(*m* 行, *n* 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。 + +**示例 1:** + +``` +输入: +[ + [ 1, 2, 3 ], + [ 4, 5, 6 ], + [ 7, 8, 9 ] +] +输出: [1,2,3,6,9,8,7,4,5] +``` + +**示例 2:** + +``` +输入: +[ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9,10,11,12] +] +输出: [1,2,3,4,8,12,11,10,9,5,6,7] +``` + +**提示**: + +- m == matrix.length +- n == matrix[i].length +- 1 <= m, n <= 10 +- -100 <= matrix[i][j] <= 100 + +**解法** + +从外往里一圈一圈遍历并存储矩阵元素即可。 + +```java +class Solution { + private List res; + + public List spiralOrder(int[][] matrix) { + int m = matrix.length, n = matrix[0].length; + res = new ArrayList<>(); + int i1 = 0, i2 = m - 1; + int j1 = 0, j2 = n - 1; + while (i1 <= i2 && j1 <= j2) { + add(matrix, i1++, j1++, i2--, j2--); + } + return res; + } + + private void add(int[][] matrix, int i1, int j1, int i2, int j2) { + if (i1 == i2) { + for (int j = j1; j <= j2; ++j) { + res.add(matrix[i1][j]); + } + return; + } + if (j1 == j2) { + for (int i = i1; i <= i2; ++i) { + res.add(matrix[i][j1]); + } + return; + } + for (int j = j1; j < j2; ++j) { + res.add(matrix[i1][j]); + } + for (int i = i1; i < i2; ++i) { + res.add(matrix[i][j2]); + } + for (int j = j2; j > j1; --j) { + res.add(matrix[i2][j]); + } + for (int i = i2; i > i1; --i) { + res.add(matrix[i][j1]); + } + } +} +``` + + + +### 两数之和 + +**题目描述** + +给定一个整数数组 `nums` 和一个目标值 `target`,请你在该数组中找出和为目标值的那 **两个** 整数,并返回他们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。 + +**示例:** + +``` +给定 nums = [2, 7, 11, 15], target = 9 + +因为 nums[0] + nums[1] = 2 + 7 = 9 +所以返回 [0, 1] +``` + +**解法** + +用哈希表(字典)存放数组值以及对应的下标。 + +遍历数组,当发现 `target - nums[i]` 在哈希表中,说明找到了目标值。 + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map map = new HashMap<>(); + for (int i = 0, n = nums.length; i < n; ++i) { + int num = target - nums[i]; + if (map.containsKey(num)) { + return new int[]{map.get(num), i}; + } + map.put(nums[i], i); + } + return null; + } +} +``` + + + + + +### 三数之和 + +给你一个包含 *n* 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 *a,b,c ,*使得 *a + b + c =* 0 ?请你找出所有满足条件且不重复的三元组。 + +**注意:**答案中不可以包含重复的三元组。 + + + +**示例:** + +``` +给定数组 nums = [-1, 0, 1, 2, -1, -4], + +满足要求的三元组集合为: +[ + [-1, 0, 1], + [-1, -1, 2] +] +``` + +**解法** + +“排序 + 双指针”实现。 + +```java +class Solution { + public List> threeSum(int[] nums) { + int n; + if (nums == null || (n = nums.length) < 3) { + return Collections.emptyList(); + } + Arrays.sort(nums); + List> res = new ArrayList<>(); + for (int i = 0; i < n - 2; ++i) { + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + int p = i + 1, q = n - 1; + while (p < q) { + if (p > i + 1 && nums[p] == nums[p - 1]) { + ++p; + continue; + } + if (q < n - 1 && nums[q] == nums[q + 1]) { + --q; + continue; + } + if (nums[p] + nums[q] + nums[i] < 0) { + ++p; + } else if (nums[p] + nums[q] + nums[i] > 0) { + --q; + } else { + res.add(Arrays.asList(nums[p], nums[q], nums[i])); + ++p; + --q; + } + } + } + return res; + } +} +``` + + + +### 四数之和 + +**题目描述** + +给定一个包含 *n* 个整数的数组 `nums` 和一个目标值 `target`,判断 `nums` 中是否存在四个元素 *a,**b,c* 和 *d* ,使得 *a* + *b* + *c* + *d* 的值与 `target` 相等?找出所有满足条件且不重复的四元组。 + +**注意:** + +答案中不可以包含重复的四元组。 + +**示例:** + +``` +给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 + +满足要求的四元组集合为: +[ + [-1, 0, 0, 1], + [-2, -1, 1, 2], + [-2, 0, 0, 2] +] +``` + +**解法** + +“排序 + 双指针”实现。 + +```java +class Solution { + public List> fourSum(int[] nums, int target) { + int n; + if (nums == null || (n = (nums.length)) < 4) { + return Collections.emptyList(); + } + Arrays.sort(nums); + List> res = new ArrayList<>(); + for (int i = 0; i < n - 3; ++i) { + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + for (int j = i + 1; j < n - 2; ++j) { + if (j > i + 1 && nums[j] == nums[j - 1]) { + continue; + } + int p = j + 1, q = n - 1; + while (p < q) { + if (p > j + 1 && nums[p] == nums[p - 1]) { + ++p; + continue; + } + if (q < n - 1 && nums[q] == nums[q + 1]) { + --q; + continue; + } + int t = nums[i] + nums[j] + nums[p] + nums[q]; + if (t == target) { + res.add(Arrays.asList(nums[i], nums[j], nums[p], nums[q])); + ++p; + --q; + } else if (t < target) { + ++p; + } else { + --q; + } + } + } + } + return res; + } +} +``` + + + +### 较小的三数之和 + +**题目描述** + +给定一个长度为 *n* 的整数数组和一个目标值 *target*,寻找能够使条件 `nums[i] + nums[j] + nums[k] < target` 成立的三元组 `i, j, k` 个数(`0 <= i < j < k < n`)。 + +**示例:** + +``` +[-2,0,1,3] +``` + +**进阶:**是否能在 *O*(*n*2) 的时间复杂度内解决? + +**解法** + +双指针解决。 + +```java +class Solution { + public int threeSumSmaller(int[] nums, int target) { + Arrays.sort(nums); + int n = nums.length; + int count = 0; + for (int i = 0; i < n - 2; ++i) { + count += threeSumSmaller(nums, i + 1, n - 1, target - nums[i]); + } + return count; + } + + private int threeSumSmaller(int[] nums, int start, int end, int target) { + int count = 0; + while (start < end) { + if (nums[start] + nums[end] < target) { + count += (end - start); + ++start; + } else { + --end; + } + } + return count; + } +} +``` + + + +### 最接近的三数之和 + +**题目描述** + +给定一个包括 *n* 个整数的数组 `nums` 和 一个目标值 `target`。找出 `nums` 中的三个整数,使得它们的和与 `target` 最接近。返回这三个数的和。假定每组输入只存在唯一答案。 + +``` +例如,给定数组 nums = [-1,2,1,-4], 和 target = 1. + +与 target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2). +``` + +**解法** + +双指针解决。 + +```java +class Solution { + public int threeSumClosest(int[] nums, int target) { + Arrays.sort(nums); + int res = 0; + int n = nums.length; + int diff = Integer.MAX_VALUE; + for (int i = 0; i < n - 2; ++i) { + int t = twoSumClosest(nums, i + 1, n - 1, target - nums[i]); + if (Math.abs(nums[i] + t - target) < diff) { + res = nums[i] + t; + diff = Math.abs(nums[i] + t - target); + } + } + return res; + } + + private int twoSumClosest(int[] nums, int start, int end, int target) { + int res = 0; + int diff = Integer.MAX_VALUE; + while (start < end) { + int val = nums[start] + nums[end]; + if (val == target) { + return val; + } + if (Math.abs(val - target) < diff) { + res = val; + diff = Math.abs(val - target); + } + if (val < target) { + ++start; + } else { + --end; + } + } + return res; + } +} +``` + + + +### 合并两个有序数组 + +**题目描述** + +给你两个有序整数数组 *nums1* 和 *nums2*,请你将 *nums2* 合并到 *nums1* 中*,*使 *num1* 成为一个有序数组。 + + + +**说明:** + +- 初始化 *nums1* 和 *nums2* 的元素数量分别为 *m* 和 *n* 。 +- 你可以假设 *nums1* 有足够的空间(空间大小大于或等于 *m + n*)来保存 *nums2* 中的元素。 + + + +**示例:** + +``` +输入: +nums1 = [1,2,3,0,0,0], m = 3 +nums2 = [2,5,6], n = 3 + +输出: [1,2,2,3,5,6] +``` + +**解法** + +双指针解决。 + +```java +class Solution { + public void merge(int[] nums1, int m, int[] nums2, int n) { + int i = m - 1, j = n - 1; + int k = m + n - 1; + while (j >= 0) { + if (i >= 0 && nums1[i] >= nums2[j]) { + nums1[k--] = nums1[i--]; + } else { + nums1[k--] = nums2[j--]; + } + } + } +} +``` + + + + + +### 寻找旋转排序数组中的最小值 + +**题目描述** + +假设按照升序排序的数组在预先未知的某个点上进行了旋转。 + +( 例如,数组 `[0,1,2,4,5,6,7]` 可能变为 `[4,5,6,7,0,1,2]` )。 + +请找出其中最小的元素。 + +你可以假设数组中不存在重复元素。 + +**示例 1:** + +``` +输入: [3,4,5,1,2] +输出: 1 +``` + +**示例 2:** + +``` +输入: [4,5,6,7,0,1,2] +输出: 0 +``` + +**解法** + +二分查找。 + +若 `nums[m] > nums[r]`,说明最小值在 m 的右边,否则说明最小值在 m 的左边(包括 m)。 + +```java +class Solution { + public int findMin(int[] nums) { + int l = 0, r = nums.length - 1; + while (l < r) { + int m = (l + r) >>> 1; + if (nums[m] > nums[r]) { + l = m + 1; + } else { + r = m; + } + } + return nums[l]; + } +} +``` + + + +### 寻找旋转排序数组中的最小值 II + +**题目描述** + +假设按照升序排序的数组在预先未知的某个点上进行了旋转。 + +( 例如,数组 `[0,1,2,4,5,6,7]` 可能变为 `[4,5,6,7,0,1,2]` )。 + +请找出其中最小的元素。 + +注意数组中可能存在重复的元素。 + +**示例 1:** + +``` +输入: [1,3,5] +输出: 1 +``` + +**示例 2:** + +``` +输入: [2,2,2,0,1] +输出: 0 +``` + +**说明:** + +- 允许重复会影响算法的时间复杂度吗?会如何影响,为什么? + +```java +class Solution { + public int findMin(int[] nums) { + int l = 0, r = nums.length - 1; + while (l < r) { + int m = (l + r) >>> 1; + if (nums[m] > nums[r]) { + l = m + 1; + } else if (nums[m] < nums[r]) { + r = m; + } else { + --r; + } + } + return nums[l]; + } +} +``` + + + +### 除自身以外数组的乘积 + +**题目描述** + +给你一个长度为 *n* 的整数数组 `nums`,其中 *n* > 1,返回输出数组 `output` ,其中 `output[i]` 等于 `nums` 中除 `nums[i]` 之外其余各元素的乘积。 + + + +**示例:** + +``` +[1,2,3,4] +``` + + + +**提示:**题目数据保证数组之中任意元素的全部前缀元素和后缀(甚至是整个数组)的乘积都在 32 位整数范围内。 + +**说明:** 请**不要使用除法,**且在 O(*n*) 时间复杂度内完成此题。 + +**进阶:** +你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组**不被视为**额外空间。) + +**解法** + +```java +class Solution { + public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int[] output = new int[n]; + for (int i = 0, left = 1; i < n; ++i) { + output[i] = left; + left *= nums[i]; + } + for (int i = n - 1, right = 1; i >= 0; --i) { + output[i] *= right; + right *= nums[i]; + } + return output; + } +} +``` + + + +### 无重复字符的最长子串 + +**题目描述** + +给定一个字符串,请你找出其中不含有重复字符的 **最长子串** 的长度。 + +**示例 1:** + +``` +"abc",所以其 +``` + +**示例 2:** + +``` +"b" +``` + +**示例 3:** + +``` +"wke" +``` + +**解法** + +- 定义一个哈希表存放字符及其出现的位置; +- 定义 i, j 分别表示不重复子串的开始位置和结束位置; +- j 向后遍历,若遇到与 `[i, j]` 区间内字符相同的元素,更新 i 的值,此时 `[i, j]` 区间内不存在重复字符,计算 res 的最大值。 + +```java +class Solution { + public int lengthOfLongestSubstring(String s) { + int res = 0; + Map chars = new HashMap<>(); + for (int i = 0, j = 0; j < s.length(); ++j) { + char c = s.charAt(j); + if (chars.containsKey(c)) { + // chars.get(c)+1 可能比 i 还小,通过 max 函数来锁住左边界 + // e.g. 在"tmmzuxt"这个字符串中,遍历到最后一步时,最后一个字符't'和第一个字符't'是相等的。如果没有 max 函数,i 就会回到第一个't'的索引0处的下一个位置 + i = Math.max(i, chars.get(c) + 1); + } + chars.put(c, j); + res = Math.max(res, j - i + 1); + } + return res; + } +} +``` + + + +### 反转字符串中的元音字母 + +**题目描述** + +编写一个函数,以字符串作为输入,反转该字符串中的元音字母。 + +**示例 1:** + +``` +输入: "hello" +输出: "holle" +``` + +**示例 2:** + +``` +输入: "leetcode" +输出: "leotcede" +``` + +**说明:** +元音字母不包含字母"y"。 + +**解法** + +将字符串转为字符数组(或列表),定义双指针 p、q,分别指向数组(列表)头部和尾部,当 p、q 指向的字符均为元音字母时,进行交换。 + +依次遍历,当 `p >= q` 时,遍历结束。将字符数组(列表)转为字符串返回即可。 + +```java +class Solution { + public String reverseVowels(String s) { + if (s == null) { + return s; + } + char[] chars = s.toCharArray(); + int p = 0, q = chars.length - 1; + while (p < q) { + if (!isVowel(chars[p])) { + ++p; + continue; + } + if (!isVowel(chars[q])) { + --q; + continue; + } + swap(chars, p++, q--); + } + return String.valueOf(chars); + } + + private void swap(char[] chars, int i, int j) { + char t = chars[i]; + chars[i] = chars[j]; + chars[j] = t; + } + + private boolean isVowel(char c) { + switch(c) { + case 'a': + case 'e': + case 'i': + case 'o': + case 'u': + case 'A': + case 'E': + case 'I': + case 'O': + case 'U': + return true; + default: + return false; + } + } +} +``` + + + +### 字符串转换整数 + +**题目描述** + +请你来实现一个 `atoi` 函数,使其能将字符串转换成整数。 + +首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。 + +当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。 + +该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。 + +注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。 + +在任何情况下,若函数不能进行有效的转换时,请返回 0。 + +**说明:** + +假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。 + +**示例 1:** + +``` +输入: "42" +输出: 42 +``` + +**示例 2:** + +``` +输入: " -42" +输出: -42 +解释: 第一个非空白字符为 '-', 它是一个负号。 + 我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。 +``` + +**示例 3:** + +``` +输入: "4193 with words" +输出: 4193 +解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。 +``` + +**示例 4:** + +``` +输入: "words and 987" +输出: 0 +解释: 第一个非空字符是 'w', 但它不是数字或正、负号。 + 因此无法执行有效的转换。 +``` + +**示例 5:** + +``` +输入: "-91283472332" +输出: -2147483648 +解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 + 因此返回 INT_MIN (−231) 。 +``` + +**解法** + +遍历字符串,注意做溢出处理。 + +```java +class Solution { + public int myAtoi(String s) { + if (s == null) return 0; + int n = s.length(); + if (n == 0) return 0; + int i = 0; + while (s.charAt(i) == ' ') { + // 仅包含空格 + if (++i == n) return 0; + } + int sign = 1; + if (s.charAt(i) == '-') sign = -1; + if (s.charAt(i) == '-' || s.charAt(i) == '+') ++i; + int res = 0, flag = Integer.MAX_VALUE / 10; + for (; i < n; ++i) { + // 非数字,跳出循环体 + if (s.charAt(i) < '0' || s.charAt(i) > '9') break; + // 溢出判断 + if (res > flag || (res == flag && s.charAt(i) > '7')) return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; + res = res * 10 + (s.charAt(i) - '0'); + } + return sign * res; + } +} +``` + + + +### 赎金信 + +**题目描述** + +给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成。如果可以构成,返回 true ;否则返回 false。 + +(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。) + +**注意:** + +你可以假设两个字符串均只含有小写字母。 + +``` +canConstruct("a", "b") -> false +canConstruct("aa", "ab") -> false +canConstruct("aa", "aab") -> true +``` + +**解法** + +用一个数组或字典 chars 存放 magazine 中每个字母出现的次数。遍历 ransomNote 中每个字母,判断 chars 是否包含即可。 + +```java +class Solution { + public boolean canConstruct(String ransomNote, String magazine) { + int[] chars = new int[26]; + for (int i = 0, n = magazine.length(); i < n; ++i) { + int idx = magazine.charAt(i) - 'a'; + ++chars[idx]; + } + for (int i = 0, n = ransomNote.length(); i < n; ++i) { + int idx = ransomNote.charAt(i) - 'a'; + if (chars[idx] == 0) return false; + --chars[idx]; + } + return true; + } +} +``` + + + +### 两数相加 + +**题目描述** + +给出两个 **非空** 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 **逆序** 的方式存储的,并且它们的每个节点只能存储 **一位** 数字。 + +如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 + +您可以假设除了数字 0 之外,这两个数都不会以 0 开头。 + +**示例:** + +``` +输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) +输出:7 -> 0 -> 8 +原因:342 + 465 = 807 +``` + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + int carry = 0; + ListNode dummy = new ListNode(-1); + ListNode cur = dummy; + while (l1 != null || l2 != null || carry != 0) { + int t = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + carry; + carry = t / 10; + cur.next = new ListNode(t % 10); + cur = cur.next; + l1 = l1 == null ? null : l1.next; + l2 = l2 == null ? null : l2.next; + } + return dummy.next; + } +} +``` + + + + + +### 两数相加 II + +**题目描述** + +给定两个**非空**链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。 + + + +你可以假设除了数字 0 之外,这两个数字都不会以零开头。 + +**进阶:** + +如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。 + +**示例:** + +``` +输入: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) +输出: 7 -> 8 -> 0 -> 7 +``` + +**解法** + +利用栈将数字逆序。 + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + Deque s1 = new ArrayDeque<>(); + Deque s2 = new ArrayDeque<>(); + for (; l1 != null; l1 = l1.next) { + s1.push(l1.val); + } + for (; l2 != null; l2 = l2.next) { + s2.push(l2.val); + } + int carry = 0; + ListNode dummy = new ListNode(-1); + while (!s1.isEmpty() || !s2.isEmpty() || carry != 0) { + carry += (s1.isEmpty() ? 0 : s1.pop()) + (s2.isEmpty() ? 0 : s2.pop()); + // 创建结点,利用头插法将结点插入链表 + ListNode node = new ListNode(carry % 10); + node.next = dummy.next; + dummy.next = node; + carry /= 10; + } + return dummy.next; + } +} +``` + + + + + +### 从尾到头打印链表 + +**题目描述** + +输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。 + +**示例 1:** + +``` +输入:head = [1,3,2] +输出:[2,3,1] +``` + +**限制:** + +- `0 <= 链表长度 <= 10000` + +**解法** + +栈实现。或者其它方式,见题解。 + + + +- 栈实现: + + ```java + /** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ + class Solution { + public int[] reversePrint(ListNode head) { + Stack s = new Stack<>(); + while (head != null) { + s.push(head.val); + head = head.next; + } + int[] res = new int[s.size()]; + int i = 0; + while (!s.isEmpty()) { + res[i++] = s.pop(); + } + return res; + } + } + ``` + + + +- 先计算链表长度 n,然后创建一个长度为 n 的结果数组。最后遍历链表,依次将节点值存放在数组上(从后往前)。 + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +class Solution { + public int[] reversePrint(ListNode head) { + if (head == null) return new int[]{}; + // 计算链表长度n + int n = 0; + ListNode cur = head; + while (cur != null) { + ++n; + cur = cur.next; + } + int[] res = new int[n]; + cur = head; + while (cur != null) { + res[--n] = cur.val; + cur = cur.next; + } + return res; + } +} +``` + + + +### 删除链表的节点 + +给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。 + +返回删除后的链表的头节点。 + +**示例 1:** + +``` +输入: head = [4,5,1,9], val = 5 +输出: [4,1,9] +解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9. +``` + +**示例 2:** + +``` +输入: head = [4,5,1,9], val = 1 +输出: [4,5,9] +解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9. +``` + +**说明:** + +- 题目保证链表中节点的值互不相同 +- 若使用 C 或 C++ 语言,你不需要 `free` 或 `delete` 被删除的节点 + +**解法** + +定义一个虚拟头节点 `dummy` 指向 `head`,`pre` 指针初始指向 `dummy`。 + +循环遍历链表,`pre` 往后移动。当指针 `pre.next` 指向的节点的值等于 `val` 时退出循环,将 `pre.next` 指向 `pre.next.next`,然后返回 `dummy.next`。 + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +class Solution { + public ListNode deleteNode(ListNode head, int val) { + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode pre = dummy; + while (pre.next != null && pre.next.val != val) { + pre = pre.next; + } + pre.next = pre.next == null ? null : pre.next.next; + return dummy.next; + } +} +``` + + + +### 删除排序链表中的重复元素 + +**题目描述** + +给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。 + +**示例 1:** + +``` +输入: 1->1->2 +输出: 1->2 +``` + +**示例 2:** + +``` +输入: 1->1->2->3->3 +输出: 1->2->3 +``` + + + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode deleteDuplicates(ListNode head) { + ListNode cur = head; + while (cur != null && cur.next != null) { + if (cur.val == cur.next.val) { + cur.next = cur.next.next; + } else { + cur = cur.next; + } + } + return head; + } +} +``` + + + +### 删除排序链表中的重复元素 II + +**题目描述** + +给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 *没有重复出现* 的数字。 + +**示例 1:** + +``` +输入: 1->2->3->3->4->4->5 +输出: 1->2->5 +``` + +**示例 2:** + +``` +输入: 1->1->1->2->3 +输出: 2->3 +``` + +**解法** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode deleteDuplicates(ListNode head) { + ListNode dummy = new ListNode(-1, head); + ListNode cur = dummy; + while (cur.next != null && cur.next.next != null) { + if (cur.next.val == cur.next.next.val) { + int val = cur.next.val; + while (cur.next != null && cur.next.val == val) { + cur.next = cur.next.next; + } + } else { + cur = cur.next; + } + } + return dummy.next; + } +} +``` + + + +### 移除链表元素 + +**题目描述** + +删除链表中等于给定值 ***val\*** 的所有节点。 + +**示例:** + +``` +输入: 1->2->6->3->4->5->6, val = 6 +输出: 1->2->3->4->5 +``` + +**解法** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode removeElements(ListNode head, int val) { + ListNode dummy = new ListNode(-1, head); + ListNode pre = dummy; + while (pre != null && pre.next != null) { + if (pre.next.val != val) pre = pre.next; + else pre.next = pre.next.next; + } + return dummy.next; + } +} +``` + + + + + +### 两两交换链表中的节点 + +**题目描述** + +给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 + +**你不能只是单纯的改变节点内部的值**,而是需要实际的进行节点交换。 + + + +**示例:** + +``` +1->2->3->4 +``` + +**解法** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode swapPairs(ListNode head) { + ListNode dummy = new ListNode(0, head); + ListNode pre = dummy, cur = head; + while (cur != null && cur.next != null) { + ListNode t = cur.next; + cur.next = t.next; + t.next = cur; + pre.next = t; + pre = cur; + cur = pre.next; + + } + return dummy.next; + } +} +``` + + + + + +### 排序链表 + +**题目描述** + +在 *O*(*n* log *n*) 时间复杂度和常数级空间复杂度下,对链表进行排序。 + +**示例 1:** + +``` +输入: 4->2->1->3 +输出: 1->2->3->4 +``` + +**示例 2:** + +``` +输入: -1->5->3->4->0 +输出: -1->0->3->4->5 +``` + +**解法** + +先用快慢指针找到链表中点,然后分成左右两个链表,递归排序左右链表。最后合并两个排序的链表即可。 + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + public ListNode sortList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode slow = head, fast = head.next; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + } + ListNode t = slow.next; + slow.next = null; + ListNode l1 = sortList(head); + ListNode l2 = sortList(t); + ListNode dummy = new ListNode(0); + ListNode cur = dummy; + while (l1 != null && l2 != null) { + if (l1.val <= l2.val) { + cur.next = l1; + l1 = l1.next; + } else { + cur.next = l2; + l2 = l2.next; + } + cur = cur.next; + } + cur.next = l1 == null ? l2 : l1; + return dummy.next; + } +} +``` + + + +### 反转链表 + +**题目描述** + +反转一个单链表。 + +**示例:** + +``` +输入: 1->2->3->4->5->NULL +输出: 5->4->3->2->1->NULL +``` + +**进阶:** +你可以迭代或递归地反转链表。你能否用两种方法解决这道题? + +**解法** + +定义指针 `p`、`q` 分别指向头节点和下一个节点,`pre` 指向头节点的前一个节点。 + +遍历链表,改变指针 `p` 指向的节点的指向,将其指向 `pre` 指针指向的节点,即 `p.next = pre`。然后 `pre` 指针指向 `p`,`p`、`q` 指针往前走。 + +当遍历结束后,返回 `pre` 指针即可。 + + + +**迭代版本** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +class Solution { + public ListNode reverseList(ListNode head) { + ListNode pre = null, p = head; + while (p != null) { + ListNode q = p.next; + p.next = pre; + pre = p; + p = q; + } + return pre; + } +} +``` + + + +**递归版本** + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode(int x) { val = x; } + * } + */ +class Solution { + public ListNode reverseList(ListNode head) { + if (head == null || head.next == null) { + return head; + } + ListNode res = reverseList(head.next); + head.next.next = head; + head.next = null; + return res; + } +} +``` + + + + + +### 二叉树的前序遍历 + +**题目描述** + +给定一个二叉树,返回它的 *前序* 遍历。 + + **示例:** + +``` +输入: [1,null,2,3] + 1 + \ + 2 + / + 3 + +输出: [1,2,3] +``` + +**进阶:** 递归算法很简单,你可以通过迭代算法完成吗? + +**解法** + +递归遍历或利用栈实现非递归遍历。 + +非递归的思路如下: + +1. 定义一个栈,先将根节点压入栈 +2. 若栈不为空,每次从栈中弹出一个节点 +3. 处理该节点 +4. 先把节点右孩子压入栈,接着把节点左孩子压入栈(如果有孩子节点) +5. 重复 2-4 +6. 返回结果 + + + +**递归** + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class Solution { + + private List res; + + public List preorderTraversal(TreeNode root) { + res = new ArrayList<>(); + preorder(root); + return res; + } + + private void preorder(TreeNode root) { + if (root != null) { + res.add(root.val); + preorder(root.left); + preorder(root.right); + } + } +} +``` + + + +**非递归** + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class Solution { + public List preorderTraversal(TreeNode root) { + if (root == null) { + return Collections.emptyList(); + } + List res = new ArrayList<>(); + Deque s = new ArrayDeque<>(); + s.push(root); + while (!s.isEmpty()) { + TreeNode node = s.pop(); + res.add(node.val); + if (node.right != null) { + s.push(node.right); + } + if (node.left != null) { + s.push(node.left); + } + } + return res; + } +} +``` + + + +### 二叉树的后序遍历 + +**题目描述** + +给定一个二叉树,返回它的 *后序* 遍历。 + +**示例:** + +``` +输入: [1,null,2,3] + 1 + \ + 2 + / + 3 + +输出: [3,2,1] +``` + +**进阶:** 递归算法很简单,你可以通过迭代算法完成吗? + +**解法** + +递归遍历或利用栈实现非递归遍历。 + +非递归的思路如下: + +先序遍历的顺序是:头、左、右,如果我们改变左右孩子的顺序,就能将顺序变成:头、右、左。 + +我们先不打印头节点,而是存放到另一个收集栈 s2 中,最后遍历结束,输出收集栈元素,即是后序遍历:左、右、头。 + + + +**递归** + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class Solution { + + private List res; + + public List postorderTraversal(TreeNode root) { + res = new ArrayList<>(); + postorder(root); + return res; + } + + private void postorder(TreeNode root) { + if (root != null) { + postorder(root.left); + postorder(root.right); + res.add(root.val); + } + } +} +``` + + + +**非递归** + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class Solution { + public List postorderTraversal(TreeNode root) { + if (root == null) { + return Collections.emptyList(); + } + Deque s1 = new ArrayDeque<>(); + List s2 = new ArrayList<>(); + s1.push(root); + while (!s1.isEmpty()) { + TreeNode node = s1.pop(); + s2.add(node.val); + if (node.left != null) { + s1.push(node.left); + } + if (node.right != null) { + s1.push(node.right); + } + } + Collections.reverse(s2); + return s2; + } +} +``` + + + +### 二叉树的中序遍历 + +**题目描述** + +给定一个二叉树,返回它的*中序* 遍历。 + +**示例:** + +``` +输入: [1,null,2,3] + 1 + \ + 2 + / + 3 + +输出: [1,3,2] +``` + +**进阶:** 递归算法很简单,你可以通过迭代算法完成吗? + +**解法** + +递归遍历或利用栈实现非递归遍历。 + +非递归的思路如下: + +1. 定义一个栈 +2. 将树的左节点依次入栈 +3. 左节点为空时,弹出栈顶元素并处理 +4. 重复 2-3 的操作 + + + +**递归** + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class Solution { + + private List res; + + public List inorderTraversal(TreeNode root) { + res = new ArrayList<>(); + inorder(root); + return res; + } + + private void inorder(TreeNode root) { + if (root != null) { + inorder(root.left); + res.add(root.val); + inorder(root.right); + } + } +} +``` + + + +**非递归** + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class Solution { + public List inorderTraversal(TreeNode root) { + if (root == null) { + return Collections.emptyList(); + } + List res = new ArrayList<>(); + Deque s = new ArrayDeque<>(); + while (root != null || !s.isEmpty()) { + if (root != null) { + s.push(root); + root = root.left; + } else { + root = s.pop(); + res.add(root.val); + root = root.right; + } + } + return res; + } +} +``` + + + +### 最小栈 + +**题目描述** + +设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。 + +- push(x) -- 将元素 x 推入栈中。 +- pop() -- 删除栈顶的元素。 +- top() -- 获取栈顶元素。 +- getMin() -- 检索栈中的最小元素。 + +**示例:** + +``` +MinStack minStack = new MinStack(); +minStack.push(-2); +minStack.push(0); +minStack.push(-3); +minStack.getMin(); --> 返回 -3. +minStack.pop(); +minStack.top(); --> 返回 0. +minStack.getMin(); --> 返回 -2. +``` + +**解法** + +```java +class MinStack { + + private Deque s; + private Deque helper; + + /** initialize your data structure here. */ + public MinStack() { + s = new ArrayDeque<>(); + helper = new ArrayDeque<>(); + } + + public void push(int x) { + s.push(x); + int element = helper.isEmpty() || x < helper.peek() ? x : helper.peek(); + helper.push(element); + } + + public void pop() { + s.pop(); + helper.pop(); + } + + public int top() { + return s.peek(); + } + + public int getMin() { + return helper.peek(); + } +} + +/** + * Your MinStack object will be instantiated and called as such: + * MinStack obj = new MinStack(); + * obj.push(x); + * obj.pop(); + * int param_3 = obj.top(); + * int param_4 = obj.getMin(); + */ +``` + + + +### 队列的最大值 + +**题目描述** + +请定义一个队列并实现函数 `max_value` 得到队列里的最大值,要求函数`max_value`、`push_back` 和 `pop_front` 的**均摊**时间复杂度都是 O(1)。 + +若队列为空,`pop_front` 和 `max_value` 需要返回 -1 + +**示例 1:** + +``` +输入: +["MaxQueue","push_back","push_back","max_value","pop_front","max_value"] +[[],[1],[2],[],[],[]] +输出: [null,null,null,2,1,2] +``` + +**示例 2:** + +``` +输入: +["MaxQueue","pop_front","max_value"] +[[],[],[]] +输出: [null,-1,-1] +``` + +**限制:** + +- `1 <= push_back,pop_front,max_value的总操作数 <= 10000` +- `1 <= value <= 10^5` + +**解法** + +利用一个辅助队列按单调顺序存储当前队列的最大值。 + +```java +class MaxQueue { + private Deque p; + private Deque q; + + public MaxQueue() { + p = new ArrayDeque<>(); + q = new ArrayDeque<>(); + } + + public int max_value() { + return q.isEmpty() ? -1 : q.peekFirst(); + } + + public void push_back(int value) { + while (!q.isEmpty() && q.peekLast() < value) { + q.pollLast(); + } + p.offerLast(value); + q.offerLast(value); + } + + public int pop_front() { + if (p.isEmpty()) return -1; + int res = p.pollFirst(); + if (q.peek() == res) q.pollFirst(); + return res; + } +} + +/** + * Your MaxQueue object will be instantiated and called as such: + * MaxQueue obj = new MaxQueue(); + * int param_1 = obj.max_value(); + * obj.push_back(value); + * int param_3 = obj.pop_front(); + */ +``` + + + +### 冒泡排序 + +冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。举个栗子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。冒泡排序的时间复杂度为O(n^2)。 + +实现代码: + +``` +public class BubbleSort { + + public static void bubbleSort(int[] arr) { + if(arr == null || arr.length == 0) + return ; + for(int i=0; ii; j--) { + if(arr[j] < arr[j-1]) { + swap(arr, j-1, j); + } + } + } + } + + + public static void swap(int[] arr, int i, int j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } +} +``` + + + +### 选择排序 + +选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。举个栗子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。选择排序的时间复杂度为O(n^2) + +实现代码: + +``` +public class SelectSort { + + public static void selectSort(int[] arr) { + if(arr == null || arr.length == 0) + return ; + int minIndex = 0; + for(int i=0; i 0 && target < arr[j-1]) { + arr[j] = arr[j-1]; + j --; + } + + //插入 + arr[j] = target; + } + + } + +} +``` + + + +### 快速排序 + +快速排序一听名字就觉得很高端,在实际应用当中快速排序确实也是表现最好的排序算法。快速排序虽然高端,但其实其思想是来自冒泡排序,冒泡排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。 + +举个栗子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之。 + +5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。 + +5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为什么?)4比5小停止。然后i扫描,8比5大停止。交换i,j位置。 + +5,3,4,6,8 然后j指针再扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。 + +4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序,最终得到有序序列。 + +上面留下来了一个问题为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。 + +快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。 + +实现代码: + +``` +public class QuickSort { + //一次划分 + public static int partition(int[] arr, int left, int right) { + int pivotKey = arr[left]; + int pivotPointer = left; + + while(left < right) { + while(left < right && arr[right] >= pivotKey) + right --; + while(left < right && arr[left] <= pivotKey) + left ++; + swap(arr, left, right); //把大的交换到右边,把小的交换到左边。 + } + swap(arr, pivotPointer, left); //最后把pivot交换到中间 + return left; + } + + public static void quickSort(int[] arr, int left, int right) { + if(left >= right) + return ; + int pivotPos = partition(arr, left, right); + quickSort(arr, left, pivotPos-1); + quickSort(arr, pivotPos+1, right); + } + + public static void sort(int[] arr) { + if(arr == null || arr.length == 0) + return ; + quickSort(arr, 0, arr.length-1); + } + + public static void swap(int[] arr, int left, int right) { + int temp = arr[left]; + arr[left] = arr[right]; + arr[right] = temp; + } + +} +``` + +其实上面的代码还可以再优化,上面代码中基准数已经在pivotKey中保存了,所以不需要每次交换都设置一个temp变量,在交换左右指针的时候只需要先后覆盖就可以了。这样既能减少空间的使用还能降低赋值运算的次数。优化代码如下: + +``` +public class QuickSort { + + /** + * 划分 + * @param arr + * @param left + * @param right + * @return + */ + public static int partition(int[] arr, int left, int right) { + int pivotKey = arr[left]; + + while(left < right) { + while(left < right && arr[right] >= pivotKey) + right --; + arr[left] = arr[right]; //把小的移动到左边 + while(left < right && arr[left] <= pivotKey) + left ++; + arr[right] = arr[left]; //把大的移动到右边 + } + arr[left] = pivotKey; //最后把pivot赋值到中间 + return left; + } + + /** + * 递归划分子序列 + * @param arr + * @param left + * @param right + */ + public static void quickSort(int[] arr, int left, int right) { + if(left >= right) + return ; + int pivotPos = partition(arr, left, right); + quickSort(arr, left, pivotPos-1); + quickSort(arr, pivotPos+1, right); + } + + public static void sort(int[] arr) { + if(arr == null || arr.length == 0) + return ; + quickSort(arr, 0, arr.length-1); + } + +} +``` + +总结快速排序的思想:冒泡+二分+递归分治,慢慢体会。。。 + + + +### 堆排序 + +堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。 + +首先,实现堆排序需要解决两个问题: + +如何由一个无序序列键成一个堆? + +如何在输出堆顶元素之后,调整剩余元素成为一个新的堆? + +第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆。 + +第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。 + +从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选即可。举个栗子: + +49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下 [![](https://camo.githubusercontent.com/1d0112677956ce41f69d80c9a90423169fa6d91eb8a95bdb6e41aa76409a076c/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f306c303479756d726932376f7a71346d64306f31387a77632f696d6167655f31626b3434306137383174696d31666667316871693132766231357567392e706e67)](https://camo.githubusercontent.com/1d0112677956ce41f69d80c9a90423169fa6d91eb8a95bdb6e41aa76409a076c/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f306c303479756d726932376f7a71346d64306f31387a77632f696d6167655f31626b3434306137383174696d31666667316871693132766231357567392e706e67) + + + +![image_1bk440lq5i051859166rlru38m.png-229.6kB](https://camo.githubusercontent.com/12414155750eb12dfc8fff05c6a01e51880a789f64a56265f08e813688db7dc1/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f6b65706774727377316d3338617779747170636e697939752f696d6167655f31626b3434306c713569303531383539313636726c727533386d2e706e67) + +实现代码: + +```java +public class HeapSort { + + /** + * 堆筛选,除了start之外,start~end均满足大顶堆的定义。 + * 调整之后start~end称为一个大顶堆。 + * @param arr 待调整数组 + * @param start 起始指针 + * @param end 结束指针 + */ + public static void heapAdjust(int[] arr, int start, int end) { + int temp = arr[start]; + + for(int i=2*start+1; i<=end; i*=2) { + //左右孩子的节点分别为2*i+1,2*i+2 + + //选择出左右孩子较小的下标 + if(i < end && arr[i] < arr[i+1]) { + i ++; + } + if(temp >= arr[i]) { + break; //已经为大顶堆,=保持稳定性。 + } + arr[start] = arr[i]; //将子节点上移 + start = i; //下一轮筛选 + } + + arr[start] = temp; //插入正确的位置 + } + + + public static void heapSort(int[] arr) { + if(arr == null || arr.length == 0) + return ; + + //建立大顶堆 + for(int i=arr.length/2; i>=0; i--) { + heapAdjust(arr, i, arr.length-1); + } + + for(int i=arr.length-1; i>=0; i--) { + swap(arr, 0, i); + heapAdjust(arr, 0, i-1); + } + + } + + public static void swap(int[] arr, int i, int j) { + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } +} +``` + + + +### 希尔排序 + +希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。希尔排序就利用了这个特点。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。 + +举个栗子: + +[![image_1bk441i0u1rjssh498k11fulu513.png-130kB](https://camo.githubusercontent.com/66d2100ef1eb1da892112279ea93076646dcad40e575d8a1a12d8d41895e7407/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f6b30356c7630386f7476746b716570366c6737676263696d2f696d6167655f31626b34343169307531726a7373683439386b313166756c753531332e706e67)](https://camo.githubusercontent.com/66d2100ef1eb1da892112279ea93076646dcad40e575d8a1a12d8d41895e7407/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f6b30356c7630386f7476746b716570366c6737676263696d2f696d6167655f31626b34343169307531726a7373683439386b313166756c753531332e706e67) + +从上述排序过程可见,希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。 + +希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。 + +实现代码: + +```java +public class ShellSort { + + /** + * 希尔排序的一趟插入 + * @param arr 待排数组 + * @param d 增量 + */ + public static void shellInsert(int[] arr, int d) { + for(int i=d; i=0 && arr[j]>temp) { //从后向前,找到比其小的数的位置 + arr[j+d] = arr[j]; //向后挪动 + j -= d; + } + + if (j != i - d) //存在比其小的数 + arr[j+d] = temp; + + } + } + + public static void shellSort(int[] arr) { + if(arr == null || arr.length == 0) + return ; + int d = arr.length / 2; + while(d >= 1) { + shellInsert(arr, d); + d /= 2; + } + } + +} +``` + + + +### 归并排序 + +归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。 + +举个栗子: [![image_1bk442k5t63t1e6d1gpv162nt8p1t.png-86.5kB](https://camo.githubusercontent.com/3b962a680feba7164a90fface616acc8ff7f51f52defa4a1556ce0422ff4e3be/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f796a75776b7a6c64773530356f6f727861727472326b72752f696d6167655f31626b3434326b357436337431653664316770763136326e74387031742e706e67)](https://camo.githubusercontent.com/3b962a680feba7164a90fface616acc8ff7f51f52defa4a1556ce0422ff4e3be/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f796a75776b7a6c64773530356f6f727861727472326b72752f696d6167655f31626b3434326b357436337431653664316770763136326e74387031742e706e67) + +实现代码: + +``` +public class MergeSort { + + public static void mergeSort(int[] arr) { + mSort(arr, 0, arr.length-1); + } + + /** + * 递归分治 + * @param arr 待排数组 + * @param left 左指针 + * @param right 右指针 + */ + public static void mSort(int[] arr, int left, int right) { + if(left >= right) + return ; + int mid = (left + right) / 2; + + mSort(arr, left, mid); //递归排序左边 + mSort(arr, mid+1, right); //递归排序右边 + merge(arr, left, mid, right); //合并 + } + + /** + * 合并两个有序数组 + * @param arr 待合并数组 + * @param left 左指针 + * @param mid 中间指针 + * @param right 右指针 + */ + public static void merge(int[] arr, int left, int mid, int right) { + //[left, mid] [mid+1, right] + int[] temp = new int[right - left + 1]; //中间数组 + + int i = left; + int j = mid + 1; + int k = 0; + while(i <= mid && j <= right) { + if(arr[i] <= arr[j]) { + temp[k++] = arr[i++]; + } + else { + temp[k++] = arr[j++]; + } + } + + while(i <= mid) { + temp[k++] = arr[i++]; + } + + while(j <= right) { + temp[k++] = arr[j++]; + } + + for(int p=0; p max) + max = ele; + } + + return max; + } + +} +``` + + + +### 桶排序 + +桶排序算是计数排序的一种改进和推广,但是网上有许多资料把计数排序和桶排序混为一谈。其实桶排序要比计数排序复杂许多。 + +对桶排序的分析和解释借鉴这位兄弟的文章(有改动):http://hxraid.iteye.com/blog/647759 + +桶排序的基本思想: + +假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。bindex=f(key)其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1> buckets = new ArrayList>(); //桶的索引 + + for(int i=0; i<10; i++) { + buckets.add(new LinkedList()); //用链表比较合适 + } + + //划分桶 + for(int i=0; i bucket : buckets) { + for(int ele : bucket) { + arr[k++] = ele; + } + } + } + + /** + * 映射函数 + * @param x + * @return + */ + public static int f(int x) { + return x / 10; + } + +} +``` + + + +### 基数排序 + +基数排序又是一种和前面排序方式不同的排序方式,基数排序不需要进行记录关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。 + +举个栗子: [![image_1bk444jv219053vk1fvnk7jrpp2n.png-230.7kB](https://camo.githubusercontent.com/ab0e339117e6818de1bb6efc0edf94edc39aed6ab16bab7f2effe3a238eaf066/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f653775706962336f746f7536737872306b68776d716c646b2f696d6167655f31626b3434346a76323139303533766b3166766e6b376a727070326e2e706e67)](https://camo.githubusercontent.com/ab0e339117e6818de1bb6efc0edf94edc39aed6ab16bab7f2effe3a238eaf066/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f653775706962336f746f7536737872306b68776d716c646b2f696d6167655f31626b3434346a76323139303533766b3166766e6b376a727070326e2e706e67) [![image_1bk4451i8s0u1i5h1qklafgvm34.png-145.5kB](https://camo.githubusercontent.com/9d7132abcba4b76eecef76e4cb8dccf7ffe0231a27366f855fa1e80f873b44ae/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f61386f3162306a78647766766573313663636f79686531652f696d6167655f31626b3434353169387330753169356831716b6c616667766d33342e706e67)](https://camo.githubusercontent.com/9d7132abcba4b76eecef76e4cb8dccf7ffe0231a27366f855fa1e80f873b44ae/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f61386f3162306a78647766766573313663636f79686531652f696d6167655f31626b3434353169387330753169356831716b6c616667766d33342e706e67) + +实现代码: + +```java +public class RadixSort { + + public static void radixSort(int[] arr) { + if(arr == null && arr.length == 0) + return ; + + int maxBit = getMaxBit(arr); + + + for(int i=1; i<=maxBit; i++) { + + List> buf = distribute(arr, i); //分配 + collecte(arr, buf); //收集 + } + + } + + /** + * 分配 + * @param arr 待分配数组 + * @param iBit 要分配第几位 + * @return + */ + public static List> distribute(int[] arr, int iBit) { + List> buf = new ArrayList>(); + for(int j=0; j<10; j++) { + buf.add(new LinkedList()); + } + for(int i=0; i> buf) { + int k = 0; + for(List bucket : buf) { + for(int ele : bucket) { + arr[k++] = ele; + } + } + + + } + + /** + * 获取最大位数 + * @param x + * @return + */ + public static int getMaxBit(int[] arr) { + int max = Integer.MIN_VALUE; + for(int ele : arr) { + int len = (ele+"").length(); + if(len > max) + max = len; + } + return max; + } + + /** + * 获取x的第n位,如果没有则为0. + * @param x + * @param n + * @return + */ + public static int getNBit(int x, int n) { + + String sx = x + ""; + if(sx.length() < n) + return 0; + else + return sx.charAt(sx.length()-n) - '0'; + } + +} +``` + + + +### 排序算法的各自的使用场景和适用场合。 + +[![image_1bk446mjdb8k1nu71p2l1nk31ss83u.png-119.3kB](https://camo.githubusercontent.com/ce5705dd23b5d8a1b0124d57755d20396ad1469c2d7874d56c29c2ea8491f99f/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f7439647135366236326a656f7573333139666e37313778702f696d6167655f31626b3434366d6a6462386b316e75373170326c316e6b333173733833752e706e67)](https://camo.githubusercontent.com/ce5705dd23b5d8a1b0124d57755d20396ad1469c2d7874d56c29c2ea8491f99f/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f7439647135366236326a656f7573333139666e37313778702f696d6167655f31626b3434366d6a6462386b316e75373170326c316e6b333173733833752e706e67) + +1. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。 +2. 上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。 +3. 基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成若干小的子序列,而后进行直接插入排序。 +4. 从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n^2)的简单排序也是稳定的。但是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根据具体需求选择。 +5. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。 + +附:基于比较排序算法时间下限为O(nlogn)的证明: + +基于比较排序下限的证明是通过决策树证明的,决策树的高度Ω(nlgn),这样就得出了比较排序的下限。 + +[![image_1bk446bqbtog4n415lp198tk2r3h.png-199.8kB](https://camo.githubusercontent.com/e147de68fbaf3ee7eaaf26b38c4313988193acea621218235295dbccc87c92eb/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f69326b78776d336c6b3339776f6e6c7236746330303033332f696d6167655f31626b343436627162746f67346e3431356c70313938746b327233682e706e67)](https://camo.githubusercontent.com/e147de68fbaf3ee7eaaf26b38c4313988193acea621218235295dbccc87c92eb/687474703a2f2f7374617469632e7a7962756c756f2e636f6d2f686f6d6973732f69326b78776d336c6b3339776f6e6c7236746330303033332f696d6167655f31626b343436627162746f67346e3431356c70313938746b327233682e706e67) + +首先要引入决策树。 首先决策树是一颗二叉树,每个节点表示元素之间一组可能的排序,它予以京进行的比较相一致,比较的结果是树的边。 先来说明一些二叉树的性质,令T是深度为d的二叉树,则T最多有2^片树叶。 具有L片树叶的二叉树的深度至少是logL。 所以,对n个元素排序的决策树必然有n!片树叶(因为n个数有n!种不同的大小关系),所以决策树的深度至少是log(n!),即至少需要log(n!)次比较。 而 log(n!)=logn+log(n-1)+log(n-2)+...+log2+log1 >=logn+log(n-1)+log(n-2)+...+log(n/2) >=(n/2)log(n/2) >=(n/2)logn-n/2 =O(nlogn)所以只用到比较的排序算法最低时间复杂度是O(nlogn)。 \ No newline at end of file diff --git "a/interviewDoc/Java/base/\347\275\221\347\273\234\345\215\217\350\256\256.md" "b/interviewDoc/Java/base/\347\275\221\347\273\234\345\215\217\350\256\256.md" new file mode 100644 index 0000000..c12ef58 --- /dev/null +++ "b/interviewDoc/Java/base/\347\275\221\347\273\234\345\215\217\350\256\256.md" @@ -0,0 +1,1188 @@ +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + +**所有问题以及答案,我都整理成了高清PDF,并且带目录:[Java面试整理高清PDF下载](https://gitee.com/tiger-a/java-interview/blob/master/interviewDoc/Java/index.md)** + + + + +- [什么是网络编程](#什么是网络编程) +- [网络编程中两个主要的问题](#网络编程中两个主要的问题) +- [网络协议是什么](#网络协议是什么) +- [为什么要对网络协议分层](#为什么要对网络协议分层) +- [计算机网络体系结构](#计算机网络体系结构) +- [什么是TCP/IP和UDP](#什么是tcpip和udp) +- [TCP/IP 四层模型](#tcpip-四层模型) +- [TCP与UDP区别:](#tcp与udp区别) +- [TCP和UDP的应用场景:](#tcp和udp的应用场景) +- [形容一下TCP和UDP](#形容一下tcp和udp) +- [从浏览器地址栏输入url到显示主页的过程](#从浏览器地址栏输入url到显示主页的过程) +- [说说OSI 七层、TCP/IP 四层的关系和区别?](#说说osi-七层tcpip-四层的关系和区别) +- [说说TCP 与 UDP 的区别?](#说说tcp-与-udp-的区别) +- [TCP 是如何实现数据的可靠性?](#tcp-是如何实现数据的可靠性) +- [你知道 TCP 如何处理拥塞吗?](#你知道-tcp-如何处理拥塞吗) +- [运行在TCP 或UDP的应用层协议分析。](#运行在tcp-或udp的应用层协议分析) +- [什么是Http协议?](#什么是http协议) +- [说说HTTP常用的状态码及其含义?](#说说http常用的状态码及其含义) +- [Http和Https的区别?](#http和https的区别) +- [讲一下 http1.1 和 http2 有什么区别?](#讲一下-http11-和-http2-有什么区别) +- [什么是http的请求体?](#什么是http的请求体) +- [HTTP的响应报文有哪些?](#http的响应报文有哪些) +- [Http中常见的header字段有哪些?](#http中常见的header字段有哪些) +- [HTTPS工作原理](#https工作原理) +- [讲一下三次握手和四次挥手全过程](#讲一下三次握手和四次挥手全过程) +- [三次握手与四次挥手](#三次握手与四次挥手) +- [为什么 TCP 链接需要三次握手,两次不可以么?](#为什么-tcp-链接需要三次握手两次不可以么) +- [用现实理解三次握手的具体细节](#用现实理解三次握手的具体细节) +- [建立连接可以两次握手吗?为什么?](#建立连接可以两次握手吗为什么) +- [为什么要四次挥手?](#为什么要四次挥手) +- [IP地址是怎样分类的,你知道吗?](#ip地址是怎样分类的你知道吗) +- [TCP 协议如何来保证传输的可靠性](#tcp-协议如何来保证传输的可靠性) +- [客户端不断进行请求链接会怎样?DDos(Distributed Denial of Service)攻击?](#客户端不断进行请求链接会怎样ddosdistributed-denial-of-service攻击) +- [GET 与 POST 的区别?](#get-与-post-的区别) +- [为什么在GET请求中会对URL进行编码?](#为什么在get请求中会对url进行编码) +- [TCP与UDP的区别](#tcp与udp的区别) +- [TCP和UDP分别对应的常见应用层协议](#tcp和udp分别对应的常见应用层协议) +- [DNS的寻址过程你知道吗?](#dns的寻址过程你知道吗) +- [TCP 的拥塞避免机制](#tcp-的拥塞避免机制) +- [什么是Socket](#什么是socket) +- [Socket属于网络的那个层面](#socket属于网络的那个层面) +- [Socket通讯的过程](#socket通讯的过程) +- [Socket和http的区别和应用场景](#socket和http的区别和应用场景) +- [一次完整的HTTP请求所经历几个步骤?](#一次完整的http请求所经历几个步骤) +- [浏览器中输入:“`www.xxx.com`” 之后都发生了什么?请详细阐述。](#浏览器中输入wwwxxxcom-之后都发生了什么请详细阐述) +- [什么是 HTTP 协议无状态协议?怎么解决Http协议无状态协议?](#什么是-http-协议无状态协议怎么解决http协议无状态协议) +- [Session、Cookie 与 Application](#sessioncookie-与-application) +- [有哪些 web 性能优化技术?](#有哪些-web-性能优化技术) +- [什么是 XSS 攻击?](#什么是-xss-攻击) +- [什么是跨站攻击CSRF?](#什么是跨站攻击csrf) +- [滑动窗口机制](#滑动窗口机制) +- [常用的HTTP方法有哪些?](#常用的http方法有哪些) +- [常见HTTP状态码](#常见http状态码) +- [SQL 注入](#sql-注入) +- [XSS 攻击](#xss-攻击) +- [OSI 网络体系结构与 TCP/IP 协议模型](#osi-网络体系结构与-tcpip-协议模型) +- [网络层的 ARP 协议工作原理?](#网络层的-arp-协议工作原理) +- [IP地址的分类](#ip地址的分类) +- [IP地址与物理地址](#ip地址与物理地址) +- [影响网络传输的因素有哪些?](#影响网络传输的因素有哪些) +- [什么是对称加密与非对称加密](#什么是对称加密与非对称加密) +- [你知道对称加密和非对称加密的区别和原理吗?](#你知道对称加密和非对称加密的区别和原理吗) +- [什么是Cookie](#什么是cookie) +- [什么是Session](#什么是session) +- [Cookie和Session对于HTTP有什么用?](#cookie和session对于http有什么用) +- [Cookie与Session区别](#cookie与session区别) + + + +### 什么是网络编程 + +- 网络编程的本质是多台计算机之间的数据交换。数据传递本身没有多大的难度,不就是把一个设备中的数据发送给其他设备,然后接受另外一个设备反馈的数据。现在的网络编程基本上都是基于请求/响应方式的,也就是一个设备发送请求数据给另外一个,然后接收另一个设备的反馈。在网络编程中,发起连接程序,也就是发送第一次请求的程序,被称作客户端(Client),等待其他程序连接的程序被称作服务器(Server)。客户端程序可以在需要的时候启动,而服务器为了能够时刻相应连接,则需要一直启动。 + +- 例如以打电话为例,首先拨号的人类似于客户端,接听电话的人必须保持电话畅通类似于服务器。连接一旦建立以后,就客户端和服务器端就可以进行数据传递了,而且两者的身份是等价的。在一些程序中,程序既有客户端功能也有服务器端功能,最常见的软件就是QQ、微信这类软件了。 + + + +### 网络编程中两个主要的问题 + +**1、**一个是如何准确的定位网络上一台或多台主机, + +**2、**另一个就是找到主机后如何可靠高效的进行数据传输。 + +- 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机。 +- 而TCP层则提供面向应用的可靠(TCP)的或非可靠(UDP)的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的。 +- 目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提 出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也 能及时得到服务。 + + + +### 网络协议是什么 + +在计算机网络要做到井井有条的交换数据,就必须遵守一些事先约定好的规则,比如交换数据的格式、是否需要发送一个应答信息。这些规则被称为网络协议。 + + + +### 为什么要对网络协议分层 + +**1、**简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。 + +**2、**灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受影响。 + +**3、**易于实现和维护。 + +**4、**促进标准化工作。分开后,每层功能可以相对简单地被描述 + + + +### 计算机网络体系结构 + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210325175839.png). + +**OSI参考模型** + +- OSI(Open System Interconnect),即开放式系统互联。一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及,推出了OSI参考模型,这样所有的公司都按照统一的标准来指定自己的网络,就可以互通互联了。 +- OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)。 + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210325175940.png) + + + +**TCP/IP参考模型** + +TCP/IP四层协议(数据链路层、网络层、传输层、应用层) + +**1、**应用层 应用层最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,TELNET等。 + +**2、**传输层 建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。 + +**3、**网络层 本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。 + +**4、**数据链路层 通过一些规程或协议来控制这些数据的传输,以保证被传输数据的正确性。实现这些规程或协议的`硬件`和软件加到物理线路,这样就构成了数据链路, + + + +### 什么是TCP/IP和UDP + +**1、**TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成对的两个之间必须建 立连接),TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达 + +**2、**UDP它是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可靠性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。 + + + +### TCP/IP 四层模型 + +- 应用层:对应于OSI参考模型的(应用层、表示层、会话层)。 + +- 传输层: 对应OSI的传输层,为应用层实体提供端到端的通信功能,保证了数据包的顺序传送及数据的完整性。 +- 网际层:对应于OSI参考模型的网络层,主要解决主机到主机的通信问题。 +- 网络接口层:与OSI参考模型的数据链路层、物理层对应。 + + + +### TCP与UDP区别: + +**1、**TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达; + +**2、**UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性; + +**3、**TCP通信类似于于要打个电话,接通了,确认身份后,才开始进行通行; + +**4、**UDP通信类似于学校广播,靠着广播播报直接进行通信。 + +**5、**TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多; + +**6、**TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。 + +**7、**TCP首部开销(20字节)比UDP首部开销(8字节)要大 + +**8、**UDP 的主机不需要维持复杂的连接状态表 + + + +### TCP和UDP的应用场景: + +对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以容忍;其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况 + + + +### 形容一下TCP和UDP + +**TCP通信可看作打电话:** + +李三(拨了个号码):喂,是王五吗? 王五:哎,您谁啊? 李三:我是李三,我想给你说点事儿,你现在方便吗? 王五:哦,我现在方便,你说吧。 甲:那我说了啊? 乙:你说吧。 (连接建立了,接下来就是说正事了…) + +**UDP通信可看为学校里的广播:** + +播音室:喂喂喂!全体操场集合 + + + +### 从浏览器地址栏输入url到显示主页的过程 + +**思路:** 这道题主要考察的知识点是HTTP的请求过程,**DNS解析,TCP三次握手,四次挥手这几个要点**,我们都可以讲下。 + +1. DNS解析,查找域名对应的IP地址。 +2. 与服务器通过三次握手,建立TCP连接 +3. 向服务器发送HTTP请求 +4. 服务器处理请求,返回网页内容 +5. 浏览器解析并渲染页面 +6. TCP四次挥手,连接结束 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/PoF8jo1PmpzZhoE6C26ico3G8O3LAOicSszL5ibsib9aJibTb0KtwnHFA657cdbO9yYRtibed4uJOuUYOxzTibd37DZQg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + + + +### 说说OSI 七层、TCP/IP 四层的关系和区别? + +**OSI 七层从下往上依次是:`物理层`、`数据链路层`、`网络层`、`传输层`、`会话层`、`表示层`、`应用层`。** + +一张图给你整明白: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/RXvHpViaz3EoYnryQ9Kc2y05brZXs3oT6BGVULKLmVRwBBGdVdadIu5TtJHLAqiaMlk7rGh7BvdicYFvSHibkHeGYw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +TCP/IP 四层从下往上依次是:`网络接口层`、`网络层`、`传输层`、`应用层`。与 OSI 七层的映射关系如下: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/RXvHpViaz3EoYnryQ9Kc2y05brZXs3oT6qJoymBkOAoMX4OGHjjT2bVWThibf2up6ktoAYNaAVSARFRO8YUN02ew/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +**特点:** + +- 层与层之间相互独立又相互依靠 +- 上层依赖于下层,下层为上层提供服务 + +**敲黑板:TCP/IP 四层是 OSI 七层的简化版,已经成为实事国际标准。** + + + +### 说说TCP 与 UDP 的区别? + +先上一张对比图: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/RXvHpViaz3EoYnryQ9Kc2y05brZXs3oT6JWGAt961MQ6b8T86GqWmAk3S6qH1lnl3eEjaZy5MjCcrGC19n2shbA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +**总结** + +- TCP 向上层提供面向连接的可靠服务 ,UDP 向上层提供无连接不可靠服务。 +- UDP 没有 TCP 传输可靠,但是可以在实时性要求搞的地方有所作为。 +- 对数据准确性要求高,速度可以相对较慢的,可以选用TCP。 + + + +### TCP 是如何实现数据的可靠性? + +一句话:通过`校验和`、`序列号`、`确认应答`、`超时重传`、`连接管理`、`流量控制`、`拥塞控制`等机制来保证可靠性。 + +**(1)校验和** + +在数据传输过程中,将发送的数据段都当做一个16位的整数,将这些整数加起来,并且前面的进位不能丢弃,补在最后,然后取反,得到校验和。 + +发送方:在发送数据之前计算校验和,并进行校验和的填充。接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方进行比较。 + +**(2)序列号** + +TCP 传输时将每个字节的数据都进行了编号,这就是序列号。序列号的作用不仅仅是应答作用,有了序列号能够将接收到的数据根据序列号进行排序,并且去掉重复的数据。 + +**(3)确认应答** + +TCP 传输过程中,每次接收方接收到数据后,都会对传输方进行确认应答,也就是发送 ACK 报文,这个 ACK 报文中带有对应的确认序列号,告诉发送方,接收了哪些数据,下一次数据从哪里传。 + +**(4)超时重传** + +在进行 TCP 传输时,由于存在确认应答与序列号机制,也就是说发送方发送一部分数据后,都会等待接收方发送的 ACK 报文,并解析 ACK 报文,判断数据是否传输成功。如果发送方发送完数据后,迟迟都没有接收到接收方传来的 ACK 报文,那么就对刚刚发送的数据进行重发。 + +**(5)连接管理** + +就是指三次握手、四次挥手的过程。 + +**(6)流量控制** + +如果发送方的发送速度太快,会导致接收方的接收缓冲区填充满了,这时候继续传输数据,就会造成大量丢包,进而引起丢包重传等等一系列问题。TCP 支持根据接收端的处理能力来决定发送端的发送速度,这就是流量控制机制。 + +具体实现方式:接收端将自己的接收缓冲区大小放入 TCP 首部的『窗口大小』字段中,通过 ACK 通知发送端。 + +**(7)拥塞控制** + +TCP 传输过程中一开始就发送大量数据,如果当时网络非常拥堵,可能会造成拥堵加剧。所以 TCP 引入了`慢启动机制`,在开始发送数据的时候,先发少量的数据探探路。 + + + +### 你知道 TCP 如何处理拥塞吗? + +网络拥塞现象是指到达通信网络中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。拥塞控制是处理网络拥塞现象的一种机制。 + +拥塞控制的四个阶段: + +- 慢启动 +- 拥塞避免 +- 快速重传 +- 快速恢复 + + + +### 运行在TCP 或UDP的应用层协议分析。 + +**运行在TCP协议上的协议:** + +- HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。 +- HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。 +- FTP(File Transfer Protocol,文件传输协议),用于文件传输。 +- POP3(Post Office Protocol, version 3,邮局协议),收邮件用。 +- SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。 +- TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。 +- SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。 + +**运行在UDP协议上的协议:** + +- BOOTP(Boot Protocol,启动协议),应用于无盘设备。 +- NTP(Network Time Protocol,网络时间协议),用于网络同步。 +- DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。 + +**运行在TCP和UDP协议上:** + +- DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。 +- ECHO(Echo Protocol,回绕协议),用于查错及测量应答时间(运行在[TCP](http://zh.wikipedia.org/zh-cn/TCP)和[UDP](http://zh.wikipedia.org/zh-cn/UDP)协议上)。 +- SNMP(Simple Network Management Protocol,简单网络管理协议),用于网络信息的收集和网络管理。 +- DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。 +- ARP(Address Resolution Protocol,地址解析协议),用于动态解析以太网硬件的地址。 + + + +### 什么是Http协议? + +- Http协议是对客户端和服务器端之间数据之间实现可靠性的传输文字、图片、音频、视频等超文本数据的规范,格式简称为“超文本传输协议” +- Http协议属于应用层,及用户访问的第一层就是http + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210326001352.png) + + + + + +### 说说HTTP常用的状态码及其含义? + +**思路:** 这道面试题主要考察候选人,是否掌握HTTP状态码这个基础知识点。 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/PoF8jo1PmpzZhoE6C26ico3G8O3LAOicSsA1ibrAEaWJLpdCxZ7zAmq1OhHJbrosmaPU3wNH85LOL6icvWIWQibUn0w/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +**不管是不是面试需要,我们都要知道,日常开发中的这几个状态码的含义哈:** + +![图片](https://mmbiz.qpic.cn/mmbiz_png/PoF8jo1PmpzZhoE6C26ico3G8O3LAOicSsSRpYffO9yCxGmQUH28ibCjkFTb0oyGDx5fGF55b0b8md1sUKMm2X52g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + +### Http和Https的区别? + +Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;Https是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的HTTP。二者之间存在如下不同: + +- 端口不同:Http与Http使用不同的连接方式,用的端口也不一样,前者是80,后者是443; +- 资源消耗:和HTTP通信相比,Https通信会由于加减密处理消耗更多的CPU和内存资源; +- 开销:Https通信需要证书,而证书一般需要向认证机构购买; + +Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。 + + + +### 讲一下 http1.1 和 http2 有什么区别? + +**HTTP1.1** + +- 持久连接 +- 请求管道化 +- 增加缓存处理(新的字段如cache-control) +- 增加 Host 字段、支持断点传输等 + +**HTTP2.0** + +- 二进制分帧 +- 多路复用(或连接共享) +- 头部压缩 +- 服务器推送 + + + +### 什么是http的请求体? + +**1、**HTTP请求体是我们请求数据时先发送给服务器的数据,毕竟我向服务器那数据,先要表明我要什么吧 + +**2、**HTTP请求体由:请求行 、请求头、请求数据组成的, + +**3、**注意:GIT请求是没有请求体的 + +**POST请求** + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210326001732.png) + + + +**GET请求是没有请求体的** + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210326001814.png) + + + + + +### HTTP的响应报文有哪些? + +**1、**http的响应报是服务器返回给我们的数据,必须先有请求体再有响应报文 + +**2、**响应报文包含三部分 状态行、响应首部字段、响应内容实体实现 + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210326001902.png) + + + +### Http中常见的header字段有哪些? + +cookie,请求时传递给服务端的cookie信息 set-cookie,响应报文首部设置要传递给客户端的cookie信息 allow,支持什么HTTP方法 last-modified,资源的最后修改时间 expires,设置资源缓存的失败日期 content-language,实体的资源语言 content-encoding,实体的编码格式 content-length,实体主体部分的大小单位是字节 content-range,返回的实体的哪些范围 content-type,哪些类型 accept-ranges,处理的范围请求 age,告诉客户端服务器在多久前创建了响应 vary,代理服务器的缓存信息 location,用于指定重定向后的URI If-Match,值是资源的唯一标识 User-Agent,将创建请求的浏览器和用户代理名称等信息传递给服务器 Transfer-Encoding,传输报文的主体编码方式 connection,管理持久连接,keep-alive , close Cache-Control,控制浏览器的强缓存 + + + +### HTTPS工作原理 + +**1、**首先HTTP请求服务端生成证书,客户端对证书的有效期、合法性、域名是否与请求的域名一致、证书的公钥(RSA加密)等进行校验; + +**2、**客户端如果校验通过后,就根据证书的公钥的有效, 生成随机数,随机数使用公钥进行加密(RSA加密); + +**3、**消息体产生的后,对它的摘要进行MD5(或者SHA1)算法加密,此时就得到了RSA签名; + +**4、**发送给服务端,此时只有服务端(RSA私钥)能解密。 + +**5、**解密得到的随机数,再用AES加密,作为密钥(此时的密钥只有客户端和服务端知道)。 + + + +### 讲一下三次握手和四次挥手全过程 + +三次握手的过程给面试官甩一张图: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/RXvHpViaz3EoYnryQ9Kc2y05brZXs3oT6X6OSEPrbr4EVuWPFXko7Pe07BXNBhsBKnrz4HUoFbmJqKmue7GLnlg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +四次挥手的过程再给面试官甩一张图: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/RXvHpViaz3EoYnryQ9Kc2y05brZXs3oT6EiaSyiajIIACBlG1q6Je5zShAjGp9tsAD28aOSN0LXZ8dH18cRuia56FQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + + + + +### 三次握手与四次挥手 + +**(1). 三次握手(我要和你建立链接,你真的要和我建立链接么,我真的要和你建立链接,成功)** + +- 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。 +- 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。 +- 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。 + +**(2). 四次挥手(我要和你断开链接;好的,断吧。我也要和你断开链接;好的,断吧):** + +- 第一次挥手:Client发送一个FIN,**用来关闭Client到Server的数据传送**,Client进入FIN_WAIT_1状态。 +- 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。此时TCP链接处于半关闭状态,即客户端已经没有要发送的数据了,但服务端若发送数据,则客户端仍要接收。 +- 第三次挥手:Server发送一个FIN,**用来关闭Server到Client的数据传送**,Server进入LAST_ACK状态。 +- 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。 + + + +### 为什么 TCP 链接需要三次握手,两次不可以么? + +“三次握手” 的目的是为了防止**已失效的链接请求报文突然又传送到了服务端**,因而产生错误。 + +- 正常的情况:A 发出连接请求,但因连接请求报文丢失而未收到确认,于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报文段,其中第一个丢失,第二个到达了 B。没有 “已失效的连接请求报文段”。 +- 现假定出现了一种异常情况:即 A 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 B。本来这是一个早已失效的报文段。但 B 收到此失效的连接请求报文段后,就误认为是 A 再次发出的一个新的连接请求。于是就向 A 发出确认报文段,同意建立连接。 + +假设不采用“三次握手”,那么只要 B 发出确认,新的连接就建立了。由于现在 A 并没有发出建立连接的请求,因此不会理睬 B 的确认,也不会向 B 发送数据。但 B 却以为新的运输连接已经建立,并一直等待 A 发来数据。这样,B 的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。 + +--- + + + +- 两次握手只能保证单向连接是畅通的 + +第一步,客户端给服务端发送一条消息:你好,服务端。第二步,服务端收到消息,同时给客户端回复一条消息:收到!你好客户端。 + +这样的两次握手过程, 客户端给服务端打招呼,服务端收到了,说明客户端可以正常给服务端发送数据。但是服务端给客户端打招呼,服务端没有收到反馈,也就不能确保服务端是否能正常给客户端发送消息。 + +- 只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据 第三步,客户端收到服务端发送的消息,回复:收到!这样就证明了客户端能正常收到服务端的消息。 + + + +### 用现实理解三次握手的具体细节 + +三次握手的目的是建立可靠的通信信道,主要的目的就是双方确认自己与对方的发送与接收机能正常。 + +**1、**第一次握手:客户什么都不能确认;服务器确认了对方发送正常 + +**2、**第二次握手:客户确认了:自己发送、接收正常,对方发送、接收正常;服务器确认 了:自己接收正常,对方发送正常 + +**3、**第三次握手:客户确认了:自己发送、接收正常,对方发送、接收正常;服务器确认 了:自己发送、接收正常,对方发送接收正常 所以三次握手就能确认双发收发功能都正常,缺一不可。 + + + +### 建立连接可以两次握手吗?为什么? + +不可以。 + +因为可能会出现已失效的连接请求报文段又传到了服务器端。 > client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用 “三次握手” 的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。 + +而且,两次握手无法保证Client正确接收第二次握手的报文(Server无法确认Client是否收到),也无法保证Client和Server之间成功互换初始序列号。 + + + + + +### 为什么要四次挥手? + +TCP 协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP 是全双工模式,这就意味着,当 A 向 B 发出 FIN 报文段时,只是表示 A 已经没有数据要发送了,而此时 A 还是能够接受到来自 B 发出的数据;B 向 A 发出 ACK 报文段也只是告诉 A ,它自己知道 A 没有数据要发了,但 B 还是能够向 A 发送数据。 + +所以想要愉快的结束这次对话就需要四次挥手。 + + + +### IP地址是怎样分类的,你知道吗? + +先说一下 IP 的基本特点: + +- IP地址由四段组成,每个字段是一个字节,8位,最大值是255。 +- IP地址由两部分组成,即网络地址和主机地址。网络地址表示其属于互联网的哪一个网络,主机地址表示其属于该网络中的哪一台主机。 + +IP 地址主要分为A、B、C三类及特殊地址D、E这五类,甩一张图: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/RXvHpViaz3EoYnryQ9Kc2y05brZXs3oT6scLHLEM9CZL3mH3PLdibXbSxo6f5MatN9yNjwYrsf4xmwzdKtsfK8sw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) + +**A类:** (1.0.0.0-126.0.0.0)一般用于大型网络。 + +**B类:** (128.0.0.0-191.255.0.0)一般用于中等规模网络。 + +**C类:** (192.0.0.0-223.255.255.0)一般用于小型网络。 + +**D类:** 是多播地址,地址的网络号取值于224~239之间,一般用于多路广播用户。 + +**E类:** 是保留地址。地址的网络号取值于240~255之间。 + + + +### TCP 协议如何来保证传输的可靠性 + +TCP 提供一种面向连接的、可靠的字节流服务。其中,面向连接意味着两个使用 TCP 的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个 TCP 连接。在一个 TCP 连接中,仅有两方进行彼此通信;而字节流服务意味着两个应用程序通过 TCP 链接交换 8 bit 字节构成的字节流,TCP 不在字节流中插入记录标识符。 + +**对于可靠性,TCP通过以下方式进行保证:** + +- **数据包校验**:目的是检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时TCP发送数据端超时后会重发数据; +- **对失序数据包重排序**:既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层; +- **丢弃重复数据**:对于重复数据,能够丢弃重复数据; +- **应答机制**:当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒; +- **超时重发**:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段; +- **流量控制**:TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。TCP使用的流量控制协议是可变大小的滑动窗口协议。 + + + +### 客户端不断进行请求链接会怎样?DDos(Distributed Denial of Service)攻击? + +服务器端会为每个请求创建一个链接,并向其发送确认报文,然后等待客户端进行确认 + +**(1). DDos 攻击:** + +- 客户端向服务端发送请求链接数据包 +- 服务端向客户端发送确认数据包 +- 客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认 + +**(2). DDos 预防:(没有彻底根治的办法,除非不使用TCP)** + +- 限制同时打开SYN半链接的数目 +- 缩短SYN半链接的Time out 时间 +- 关闭不必要的服务 + + + +### GET 与 POST 的区别? + +GET与POST是我们常用的两种HTTP Method,二者之间的区别主要包括如下五个方面: + +**1、** 从功能上讲,GET一般用来从服务器上获取资源,POST一般用来更新服务器上的资源; + +**2、**从REST服务角度上说,GET是幂等的,即读取同一个资源,总是得到相同的数据,而POST不是幂等的,因为每次请求对资源的改变并不是相同的;进一步地,GET不会改变服务器上的资源,而POST会对服务器资源进行改变; + +**3、**从请求参数形式上看,GET请求的数据会附在URL之后,即将请求数据放置在HTTP报文的 请求头 中,以?分割URL和传输数据,参数之间以&相连。特别地,如果数据是英文字母/数字,原样发送;否则,会将其编码为 application/x-www-form-urlencoded MIME 字符串(如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如:%E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII);而POST请求会把提交的数据则放置在是HTTP请求报文的 请求体 中。 + +**4、**就安全性而言,POST的安全性要比GET的安全性高,因为GET请求提交的数据将明文出现在URL上,而且POST请求参数则被包装到请求体中,相对更安全。 + +**5、**从请求的大小看,GET请求的长度受限于浏览器或服务器对URL长度的限制,允许发送的数据量比较小,而POST请求则是没有大小限制的。 + + + +### 为什么在GET请求中会对URL进行编码? + +我们知道,在GET请求中会对URL中非西文字符进行编码,这样做的目的就是为了 **避免歧义**。看下面的例子, + +针对 “name1=value1&name2=value2” 的例子,我们来谈一下数据从客户端到服务端的解析过程。首先,上述字符串在计算机中用ASCII吗表示为: + +``` + 6E616D6531 3D 76616C756531 26 6E616D6532 3D 76616C756532 6E616D6531:name1 3D:= 76616C756531:value1 26:& 6E616D6532:name2 3D:= 76616C756532:value2复制代码 +``` + +服务端在接收到该数据后就可以遍历该字节流,一个字节一个字节的吃,当吃到3D这字节后,服务端就知道前面吃得字节表示一个key,再往后吃,如果遇到26,说明从刚才吃的3D到26子节之间的是上一个key的value,以此类推就可以解析出客户端传过来的参数。 + +现在考虑这样一个问题,如果我们的参数值中就包含=或&这种特殊字符的时候该怎么办?比如,“name1=value1”,其中value1的值是“va&lu=e1”字符串,那么实际在传输过程中就会变成这样“name1=va&lu=e1”。这样,我们的本意是只有一个键值对,但是服务端却会解析成两个键值对,这样就产生了歧义。 + +那么,如何解决上述问题带来的歧义呢?解决的办法就是对参数进行URL编码:例如,我们对上述会产生歧义的字符进行URL编码后结果:“name1=va%26lu%3D”,这样服务端会把紧跟在“%”后的字节当成普通的字节,就是不会把它当成各个参数或键值对的分隔符。 + + + +### TCP与UDP的区别 + +TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议,它们之间的区别包括: + +- TCP是面向连接的,UDP是无连接的; +- TCP是可靠的,UDP是不可靠的; +- TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多的通信模式; +- TCP是面向字节流的,UDP是面向报文的; +- TCP有拥塞控制机制;UDP没有拥塞控制,适合媒体通信; +- TCP首部开销(20个字节)比UDP的首部开销(8个字节)要大; + + + +### TCP和UDP分别对应的常见应用层协议 + +**1、 TCP 对应的应用层协议:** + +- **FTP**:定义了文件传输协议,使用21端口。常说某某计算机开了FTP服务便是启动了文件传输服务。下载文件,上传主页,都要用到FTP服务。 +- **Telnet**:它是一种用于远程登陆的端口,用户可以以自己的身份远程连接到计算机上,通过这种端口可以提供一种基于DOS模式下的通信服务。如以前的BBS是-纯字符界面的,支持BBS的服务器将23端口打开,对外提供服务。 +- **SMTP**:定义了简单邮件传送协议,现在很多邮件服务器都用的是这个协议,用于发送邮件。如常见的免费邮件服务中用的就是这个邮件服务端口,所以在电子邮件设置-中常看到有这么SMTP端口设置这个栏,服务器开放的是25号端口。 +- **POP3**:它是和SMTP对应,POP3用于接收邮件。通常情况下,POP3协议所用的是110端口。也是说,只要你有相应的使用POP3协议的程序(例如Fo-xmail或Outlook),就可以不以Web方式登陆进邮箱界面,直接用邮件程序就可以收到邮件(如是163邮箱就没有必要先进入网易网站,再进入自己的邮-箱来收信)。 +- **HTTP**:从Web服务器传输超文本到本地浏览器的传送协议。 + +**2、 UDP 对应的应用层协议:** + +- **DNS**:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。 +- **SNMP**:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势。 +- **TFTP(Trival File Transfer Protocal)**:简单文件传输协议,该协议在熟知端口69上使用UDP服务 + + + +### DNS的寻址过程你知道吗? + +**(1)** 在浏览器中输入`www.baidu.com`域名,操作系统会先检查自己本地的 hosts 文件是否有这个网址映射关系,如果有就先调用这个IP地址映射,完成域名解析。 + +**(2)** 如果 hosts 里没有这个域名的映射,则查找本地 DNS 解析器缓存,是否有这个网址映射关系,如果有直接返回,完成域名解析。 + +**(3)** 如果 hosts 与本地 DNS 解析器缓存都没有相应的网址映射关系,首先会找 TCP/IP 参数中设置的首选 DNS 服务器,在此我们叫它本地 DNS 服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。 + +**(4)** 如果要查询的域名,不由本地 DNS 服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析,此解析不具有权威性。 + +**(5)** 如果本地 DNS 服务器本地区域文件与缓存解析都失效,则根据本地 DNS 服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地 DNS 就把请求发至13台根 DNS ,根 DNS 服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地 DNS 服务器收到IP信息后,将会联系负责 .com 域的这台服务器。这台负责 .com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(baidu.com)给本地 DNS 服务器。当本地 DNS 服务器收到这个地址后,就会找 baidu.com 域服务器,重复上面的动作,进行查询,直至找到 www.baidu.com 主机。 + +**(6)** 如果用的是转发模式,此 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根 DNS 或把转请求转至上上级,以此循环。不管是本地 DNS 服务器用是是转发,还是根提示,最后都是把结果返回给本地 DNS 服务器,由此 DNS 服务器再返回给客户机。 + + + + + +### TCP 的拥塞避免机制 + +拥塞:对资源的需求超过了可用的资源。若网络中许多资源同时供应不足,网络的性能就要明显变坏,整个网络的吞吐量随之负荷的增大而下降。 + +拥塞控制:防止过多的数据注入到网络中,使得网络中的路由器或链路不致过载。 + +拥塞控制的方法:. + +**1、 慢启动 + 拥塞避免:** + +**慢启动**:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小; + + + +**拥塞避免**:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长。 + + + +**2、快重传 + 快恢复:** + +**快重传**:快重传要求接收方在收到一个 **失序的报文段** 后就立即发出 **重复确认**(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。 + + + +**快恢复**:快重传配合使用的还有快恢复算法,当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半,但是接下去并不执行慢开始算法:因为如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。 + + + +### 什么是Socket + +**1、**网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。 + +**2、**但是,Socket所支持的协议种类也不光TCP/IP、UDP,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。 + +**3、**socket连接就是所谓的长连接,客户端和服务器需要互相连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉的,但是有时候网络波动还是有可能的 + +**4、**Socket偏向于底层。一般很少直接使用Socket来编程,框架底层使用Socket比较多, + + + +### Socket属于网络的那个层面 + +![](https://gitee.com/gsjqwyl/images_repo/raw/master/2021-3-11/20210325212441.png) + +Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。 + + + +### Socket通讯的过程 + +**基于TCP:** + +服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。 + +**基于UDP:** + +UDP 协议是用户数据报协议的简称,也用于网络数据的传输。虽然 UDP 协议是一种不太可靠的协议,但有时在需要较快地接收数据并且可以忍受较小错误的情况下,UDP 就会表现出更大的优势。我客户端只需要发送,服务端能不能接收的到我不管 + + + +### Socket和http的区别和应用场景 + +**1、**Socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉; + +**2、**Socket适用场景:网络游戏,银行持续交互,直播,在线视屏等。 + +**3、**http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断开等待下次连接 + +**4、**http适用场景:公司OA服务,互联网服务,电商,办公,网站等等等等 + + + + +### 一次完整的HTTP请求所经历几个步骤? + +HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤: + +**1、**建立TCP连接 + +怎么建立连接的,看上面的三次握手 + +**2、**Web浏览器向Web服务器发送请求行 + +一旦建立了TCP连接,**Web浏览器就会向Web服务器发送请求命令**。例如:GET /sample/hello.jsp HTTP/1.1。 + +**3、**Web浏览器发送请求头 + +浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,**之后浏览器发送了一空白行来通知服务器**,它已经结束了该头信息的发送。 + +**4、**Web服务器应答 + +客户机向服务器发出请求后,服务器会客户机回送应答, **HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。** + +**5、**Web服务器发送应答头 + +正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。 + +**6、**Web服务器向浏览器发送数据 + +Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,**它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据**。 + +**7、**Web服务器关闭TCP连接 + + + + + + +### 浏览器中输入:“`www.xxx.com`” 之后都发生了什么?请详细阐述。 + +解析:经典的网络协议问题。 + +**1、**由域名→IP地址 寻找IP地址的过程依次经过了浏览器缓存、系统缓存、hosts文件、路由器缓存、 递归搜索根域名服务器。 + +**2、**建立TCP/IP连接(三次握手具体过程) + +**3、**由浏览器发送一个HTTP请求 + +**4、**经过路由器的转发,通过服务器的防火墙,该HTTP请求到达了服务器 + +**5、**服务器处理该HTTP请求,返回一个HTML文件 + +**6、**浏览器解析该HTML文件,并且显示在浏览器端 + +**7、**这里需要注意: + +- HTTP协议是一种基于TCP/IP的应用层协议,进行HTTP数据请求必须先建立TCP/IP连接 +- 可以这样理解:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。 +- 两个计算机之间的交流无非是两个端口之间的数据通信,具体的数据会以什么样的形式展现是以不同的应用层协议来定义的。 + + + +### 什么是 HTTP 协议无状态协议?怎么解决Http协议无状态协议? + +HTTP 是一个无状态的协议,也就是没有记忆力,这意味着每一次的请求都是独立的,缺少状态意味着如果后续处理需要前面的信息,则它必须要重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就很快。 + +HTTP 的这种特性有优点也有缺点: + +- **优点**:解放了服务器,每一次的请求“点到为止”,不会造成不必要的连接占用 +- **缺点**:每次请求会传输大量重复的内容信息,并且,在请求之间无法实现数据的共享 + +解决方案: + +1. 使用参数传递机制: + 将参数拼接在请求的 URL 后面,实现数据的传递(GET方式),例如:`/param/list?username=wmyskxz` + **问题**:可以解决数据共享的问题,但是这种方式一不安全,二数据允许传输量只有1kb +2. 使用 Cookie 技术 +3. 使用 Session 技术 + + + +### Session、Cookie 与 Application + +Cookie和Session都是客户端与服务器之间保持状态的解决方案,具体来说,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。 + +**1、Cookie 及其相关 API :** + +Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,而客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。 + + + +**2、Session 及其相关 API:** + +同样地,会话状态也可以保存在服务器端。客户端请求服务器,如果服务器记录该用户状态,就获取Session来保存状态,这时,如果服务器已经为此客户端创建过session,服务器就按照sessionid把这个session检索出来使用;如果客户端请求不包含sessionid,则为此客户端创建一个session并且生成一个与此session相关联的sessionid,并将这个sessionid在本次响应中返回给客户端保存。保存这个sessionid的方式可以采用 **cookie机制** ,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器;若浏览器禁用Cookie的话,可以通过 **URL重写机制**将sessionid传回服务器。 + + + +**3、 Session 与 Cookie 的对比:** + +- **实现机制**:Session的实现常常依赖于Cookie机制,通过Cookie机制回传SessionID; +- **大小限制**:Cookie有大小限制并且浏览器对每个站点也有cookie的个数限制,Session没有大小限制,理论上只与服务器的内存大小有关; +- **安全性**:Cookie存在安全隐患,通过拦截或本地文件找得到cookie后可以进行攻击,而Session由于保存在服务器端,相对更加安全; +- **服务器资源消耗**:Session是保存在服务器端上会存在一段时间才会消失,如果session过多会增加服务器的压力。 + + + +**4、Application:** + +Application(ServletContext):与一个Web应用程序相对应,为应用程序提供了一个全局的状态,所有客户都可以使用该状态。 + + + +### 有哪些 web 性能优化技术? + +- DNS查询优化 +- 客户端缓存 +- 优化TCP连接 +- 避免重定向 +- 网络边缘的缓存 +- 条件缓存 +- 压缩和代码极简化 +- 图片优化 + + + +### 什么是 XSS 攻击? + +**XSS 即(Cross Site Scripting)中文名称为:跨站脚本攻击**。XSS的重点不在于跨站点,而在于脚本的执行。 + +XSS的原理是: + +恶意攻击者在web页面中会插入一些恶意的script代码。当用户浏览该页面的时候,那么嵌入到web页面中script代码会执行,因此会达到恶意攻击用户的目的。 + +XSS攻击最主要有如下分类:`反射型`、`存储型`、及 `DOM-based型`。反射性和DOM-baseed型可以归类为`非持久性XSS攻击`。存储型可以归类为`持久性XSS攻击`。 + + + +### 什么是跨站攻击CSRF? + +CSRF(Cross Site Request Forgery,跨站域请求伪造)是一种网络的攻击方式,它在 2007 年曾被列为互联网 20 大安全隐患之一,也被称为『One Click Attack』或者 『Session Riding』,通常缩写为`CSRF`或者`XSRF`,是一种对网站的恶意利用。 + +听起来像跨站脚本(XSS),但它与XSS非常不同,并且攻击方式几乎相左。 + +XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。 + + + + + +### 滑动窗口机制 + +由发送方和接收方在三次握手阶段,互相将自己的最大可接收的数据量告诉对方。 + +也就是自己的数据接收缓冲池的大小。这样对方可以根据已发送的数据量来计算是否可以接着发送。在处理过程中,当接收缓冲池的大小发生变化时,要给对方发送更新窗口大小的通知。这就实现了流量的控制。 + + + +### 常用的HTTP方法有哪些? + +- **GET:**用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器 +- **POST:**用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。 +- **PUT:**传输文件,报文主体中包含文件内容,保存到对应URI位置。 +- **HEAD:**获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。 +- **DELETE:**删除文件,与PUT方法相反,删除对应URI位置的文件。 +- **OPTIONS:**查询相应URI支持的HTTP方法。 + + + +### 常见HTTP状态码 + +**1、**1xx(临时响应) + +**2、**2xx(成功) + +**3、**3xx(重定向):表示要完成请求需要进一步操作 + +**4、**4xx(错误):表示请求可能出错,妨碍了服务器的处理 + +**5、**5xx(服务器错误):表示服务器在尝试处理请求时发生内部错误 + + + +**常见状态码:** + +- 200(成功) +- 304(未修改):自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容 +- 401(未授权):请求要求身份验证 +- 403(禁止):服务器拒绝请求 +- 404(未找到):服务器找不到请求的网页 + + + +### SQL 注入 + +SQL注入就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。 + +**1、SQL注入攻击的总体思路:** + +1. 寻找到SQL注入的位置 +2. 判断服务器类型和后台数据库类型 +3. 针对不通的服务器和数据库特点进行SQL注入攻击 + +**2、SQL注入攻击实例:** + +比如,在一个登录界面,要求输入用户名和密码,可以这样输入实现免帐号登录: + +``` +用户名: ‘or 1 = 1 --密 码:复制代码 +``` + +用户一旦点击登录,如若没有做特殊处理,那么这个非法用户就很得意的登陆进去了。这是为什么呢?下面我们分析一下:从理论上说,后台认证程序中会有如下的SQL语句: + +``` +String sql = “select * from user_table where username=’ “+userName+” ’ and password=’ “+password+” ‘”; +``` + +因此,当输入了上面的用户名和密码,上面的SQL语句变成: + +``` +SELECT * FROM user_table WHERE username=’’or 1 = 1 – and password=’’ +``` + +分析上述SQL语句我们知道,username=‘ or 1=1 这个语句一定会成功;然后后面加两个-,这意味着注释,它将后面的语句注释,让他们不起作用。这样,上述语句永远都能正确执行,用户轻易骗过系统,获取合法身份。 + +**3、应对方法:** + +1.参数绑定: + +使用预编译手段,绑定参数是最好的防SQL注入的方法。目前许多的ORM框架及JDBC等都实现了SQL预编译和参数绑定功能,攻击者的恶意SQL会被当做SQL的参数而不是SQL命令被执行。在mybatis的mapper文件中,对于传递的参数我们一般是使用#和 + +``` +不能识别此Latex公式:来获取参数值。当使用#时,变量是占位符,就是一般我们使用javajdbc的PrepareStatement时的占位符,所有可以防止sql注入;当使用复制代码 +``` + +时,变量就是直接追加在sql中,一般会有sql注入问题。 + +2.使用正则表达式过滤传入的参数 + + + +### XSS 攻击 + +XSS是一种经常出现在web应用中的计算机安全漏洞,与SQL注入一起成为web中最主流的攻击方式。XSS是指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些脚本代码嵌入到web页面中去,使别的用户访问都会执行相应的嵌入代码,从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。 + +**1、XSS攻击的危害:** + +- 盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号 +- 控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力 +- 盗窃企业重要的具有商业价值的资料 +- 非法转账 +- 强制发送电子邮件 +- 网站挂马 +- 控制受害者机器向其它网站发起攻击 + +**2、原因解析:** + +- **主要原因**:过于信任客户端提交的数据! +- **解决办法**:不信任任何客户端提交的数据,只要是客户端提交的数据就应该先进行相应的过滤处理然后方可进行下一步的操作。 +- **进一步分析细节**:客户端提交的数据本来就是应用所需要的,但是恶意攻击者利用网站对客户端提交数据的信任,在数据中插入一些符号以及javascript代码,那么这些数据将会成为应用代码中的一部分了,那么攻击者就可以肆无忌惮地展开攻击啦,因此我们绝不可以信任任何客户端提交的数据!!! + +**3、XSS 攻击分类:** + +- 1. 反射性 XSS 攻击(非持久性 XSS 攻击): + +漏洞产生的原因是攻击者注入的数据反映在响应中。一个典型的非持久性XSS攻击包含一个带XSS攻击向量的链接(即每次攻击需要用户的点击),例如,正常发送消息: + +``` +http://www.test.com/message.php?send=Hello,World!复制代码 +``` + +接收者将会接收信息并显示Hello,World;但是,非正常发送消息: + +``` +http://www.test.com/message.php?send=!复制代码 +``` + +接收者接收消息显示的时候将会弹出警告窗口! + +- 2. 持久性XSS攻击 (留言板场景): + +XSS攻击向量(一般指XSS攻击代码)存储在网站数据库,当一个页面被用户打开的时候执行。也就是说,每当用户使用浏览器打开指定页面时,脚本便执行。与非持久性XSS攻击相比,持久性XSS攻击危害性更大。从名字就可以了解到,持久性XSS攻击就是将攻击代码存入数据库中,然后客户端打开时就执行这些攻击代码。 + +例如,留言板表单中的表单域: + +``` +复制代码 +``` + +正常操作流程是:用户是提交相应留言信息 —— 将数据存储到数据库 —— 其他用户访问留言板,应用去数据并显示;而非正常操作流程是攻击者在value填写: + +``` + 复制代码 +``` + +并将数据提交、存储到数据库中;当其他用户取出数据显示的时候,将会执行这些攻击性代码。 + +**4、修复漏洞方针:** + +漏洞产生的根本原因是 **太相信用户提交的数据,对用户所提交的数据过滤不足所导致的**,因此解决方案也应该从这个方面入手,具体方案包括: + +- 将重要的cookie标记为http only, 这样的话Javascript 中的document.cookie语句就不能获取到cookie了(如果在cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样能有效的防止XSS攻击); +- 表单数据规定值的类型,例如:年龄应为只能为int、name只能为字母数字组合。。。。 +- 对数据进行Html Encode 处理 +- 过滤或移除特殊的Html标签,例如: `