Appearance
Java 基础面试题
String 是最基本的数据类型吗?
答:Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
请说说你对反射的了解?
得分点 反射概念,通过反射机制可以实现什么
Java 程序中,许多对象在运行时都会有编译时异常和运行时异常两种,例如多态情况下Car c = new Audi();这行代码运行时会生成一个 c 变量,在编译时该变量的类型是 Car,运行时该变量类型为 Audi;另外还有更极端的情况,例如程序在运行时接收到了外部传入的一个对象,这个对象的编译时类型是 Object,但程序又需要调用这个对象运行时类型的方法,这种情况下,有两种解决方法:
第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用 instanceof 运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量。
第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
具体来说,通过反射机制,我们可以实现如下的操作:
- 程序运行时,可以通过反射获得任意一个类的 Class 对象,并通过这个对象查看这个类的信息;
- 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
- 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。
加分回答
Java 的反射机制在实际项目中应用广泛,常见的应用场景有:
- 使用 JDBC 时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
- 多数框架都支持注解/XML 配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
- 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。
java 中四种修饰符的限制范围
private < 默认 < protected < public
- public: 任意包下任意类都可以访问;
- protected: 任意包下任意子类都可以访问或同包下的任意类
- 默认(包权限): 同包下的任意类都可以访问
- private: 只能在本类中使用
Object 类中的方法
toString(), equals(), clone(), getClass(), hashCode(), wait(), notify(), notifyAll()
8、接口和抽象类的区别。(注意 JDK8 的接口可以有实现)
相同点:都不能创建对象,都可以定义抽象方法
不同点:
- 抽象类可以定义成员变量(和常量);接口不能定义成员变量,但可以定义常量
- 抽象类可以有构造方法,接口不能有构造方法
- 抽象类只能单继承,接口是可以多现实
- 抽象类的成员没有修饰符,接口中的成员有默认修饰符;
- 如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法;现实接口的类必须实现所有抽象方法
jdk1.8 之后,接口可以定义非抽象方法,实现类不需要现实该方法
动态代理的两种方式,以及区别
实现方式有两种,一种是基于接口(使用 JDK 的 Proxy 类),一种是基于子类(使用第三方 jar 包 cglib)
- JDK 动态接口代理:通过反射去调用目标类的代码,将增加部分的代码。Proxy 类利用 InvocationHandler 动态创建一个接口的实例
- CGlib 动态代理:与 JDK 的方法主要区别在于,在运行期动态生成新的 class
Java 序列化的方式
- ObjectOutputStream:提供序列化对象并把其写入流的方法
- ObjectInputStream:读取流并反序列化对象
- Serializable:一个对象想要被序列化,那么它的类就要实现 此接口,这个对象的所有属性(包括 private 属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。
- Externalizable:他是 Serializable 接口的子类,有时我们不希望序列化那么多,可以使用这个接口,这个接口的 writeExternal()和 readExternal()方法可以指定序列化哪些属性;
传值和传引用的区别,Java 是怎么样的,有没有传值引用
传值,作为方法的形参,方法中修改值不会影响原来的值
传引用,作为方法的形参,实参与形参都是引用同一个内存地址,所以方法中修改会改变原来的值
传值引用??
Java 集合面试题
一个 ArrayList 在循环过程中删除,会不会出问题,为什么
方式 1:如果使用 for 循环中删除一个元素,因为 ArrayList 的删除元素是重新复制新的数组,所以原来的数组的 size 就减 1,此时索引+1,就会出现跳过下一个元素遍历的情况
注:如果使用倒序遍历时即使发生元素删除也不影响后序元素遍历。
方式 2:foreach 原理是因为这些集合类都实现了 Iterable 接口,该接口中定义了 Iterator 迭代器的产生方法,并且 foreach 就是通过 Iterable 接口在序列中进行移动。也就是说:在编译的时候编译器会自动将对 for 这个关键字的使用转化为对目标的迭代器的使用,forEach 循环删除报错
方式 3:用 Iterator 循环删除的时候,调用的是 ArrayList 里面的 remove 方法,删除元素后 modCount 会增加,expectedModCount 则不变,这样就造成了 expectedModCount != modCount,那么就抛出异常了。
总结:Iterator 调用 ArrayList 的删除方法报错,Iterator 调用迭代器自己的删除方法,单线程不会报错,多线程会报错。
Map 的常用实现类 HashMap / LinkedHashMap / TreeMap 的区别
- HashMap: 基于哈希表的 Map 接口的实现类,并允许使用 null 值和 null 键(HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。),键是唯一,存储和取出没有顺序。
- LinkedHashMap: 基于哈希表的 Map 接口的实现类,并允许使用 null 值和 null 键,键是唯一,存储和取出有顺序
- TreeMap:从功能上讲,TreeMap 有着比 HashMap 更为强大的功能,它实现了 SortedMap 接口,这意味着它可以对元素进行排序。TreeMap 的性能略微低于 HashMap。如果在开发中需要对元素进行排序,那么使用 HashMap 便无法实现这种功能,使用 TreeMap 的迭代输出将会以元素顺序进行。LinkedHashMap 是基于元素进入集合的顺序或者被访问的先后顺序排序,TreeMap 则是基于元素的固有顺序 (由 Comparator 或者 Comparable 确定)。即:LinkedHashMap 是根据元素增加或者访问的先后顺序进行排序,而 TreeMap 则根据元素的 Key 进行排序。
说说 HashMap 底层原理?
得分点 数据结构、put()流程、扩容机制
数据结构 在 JDK8 中,HashMap 底层是采用“数组+链表+红黑树”来实现的。
HashMap 是基于哈希算法来确定元素的位置(槽)的,当我们向集合中存入数据时,它会计算传入的 Key 的哈希值,并利用哈希值取余来确定槽的位置。如果元素发生碰撞,也就是这个槽已经存在其他的元素了,则 HashMap 会通过链表将这些元素组织起来。如果碰撞进一步加剧,某个链表的长度达到了 8,则 HashMap 会创建红黑树来代替这个链表,从而提高对这个槽中数据的查找的速度。
HashMap 中,数组的默认初始容量为 16,这个容量会以 2 的指数进行扩容。具体来说,当数组中的元素达到一定比例的时候 HashMap 就会扩容,这个比例叫做负载因子,默认为 0.75。自动扩容机制,是为了保证 HashMap 初始时不必占据太大的内存,而在使用期间又可以实时保证有足够大的空间。采用 2 的指数进行扩容,是为了利用位运算,提高扩容运算的效率。
put()方法的执行过程中,主要包含四个步骤:
- 判断数组,若发现数组为空,则进行首次扩容。
- 判断头节点,若发现头节点为空,则新建链表节点,存入数组。
- 判断头节点,若发现头节点非空,则将元素插入槽内。其中,第 3 步又可以细分为如下三个小步骤:
- 若元素的 key 与头节点一致,则直接覆盖头节点。
- 若元素为树型节点,则将元素追加到树中。
- 若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表长度以决定是否转为红黑树。若链表长度达到 8、数组容量未达到 64,则扩容。若链表长度达到 8、数组容量达到 64,则转为红黑树。
- 插入元素后,判断元素的个数,若发现超过阈值则再次扩容。
扩容机制 向 HashMap 中添加数据时,有三个条件会触发它的扩容行为:
- 如果数组为空,则进行首次扩容。
- 将元素接入链表后,如果链表长度达到 8,并且数组长度小于 64,则扩容。
- 添加后,如果数组中元素超过阈值,即比例超出限制(默认为 0.75),则扩容。
并且,每次扩容时都是将容量翻倍,即创建一个 2 倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于 HashMap 中数组的容量为 2^N,所以可以用位移运算计算新容量,效率很高。
加分回答
HashMap 是非线程安全的,在多线程环境下,多个线程同时触发 HashMap 的改变时,有可能会发生冲突。所以,在多线程环境下不建议使用 HashMap,可以考虑使用 Collections 将 HashMap 转为线程安全的 HashMap,更为推荐的方式则是使用 ConcurrentHashMap。
说说 ConcurrentHashMap?
得分点 数组+链表+红黑树、锁的粒度
在 JDK8 中,ConcurrentHashMap 的底层数据结构与 HashMap 一样,也是采用“数组+链表+红黑树”的形式。同时,它又采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。底层数据结构的逻辑可以参考 HashMap 的实现,下面我重点介绍它的线程安全的实现机制。
- 初始化数组或头节点时,ConcurrentHashMap 并没有加锁,而是 CAS 的方式进行原子替换(原子操作,基于 Unsafe 类的原子操作 API)。
- 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap 中锁的粒度是槽,而不是整个数组,并发的性能很好。
- 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。每个线程需先以 CAS 操作抢任务,争抢一段连续槽位的数据转移权。抢到任务后,该线程会锁定槽内的头节点,然后将链表或树中的数据迁移到新的数组里。
- 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。如果某个槽还未进行迁移,则直接可以从旧数组里找到数据。如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。
加分回答
ConcurrentHashMap 实现线程安全的难点在于多线程并发扩容,即当一个线程在插入数据时,若发现数组正在扩容,那么它就会立即参与扩容操作,完成扩容后再插入数据到新数组。在扩容的时候,多个线程共同分担数据迁移任务,每个线程负责的迁移数量是 (数组长度 >>> 3) / CPU核心数。 也就是说,为线程分配的迁移任务,是充分考虑了硬件的处理能力的。多个线程依据硬件的处理能力,平均分摊一部分槽的迁移工作。另外,如果计算出来的迁移数量小于 16,则强制将其改为 16,这是考虑到目前服务器领域主流的 CPU 运行速度,每次处理的任务过少,对于 CPU 的算力也是一种浪费。
HashMap 、ConcurrentHashMap 和 Hashtable 的区别
- 线程安全:HashMap 是线程不安全;而 ConcurrentHashMap 和 Hashtable 是线程安全的。
- key 是否允许有 null 值:HashMap 允许有 null,但只能存在一个;而 ConcurrentHashMap 和 Hashtable 都不允许。
你知道哪些线程安全的集合?
得分点 Collections、java.util.concurrent (JUC)
java.util 包下的集合类中,大部分都是非线程安全的,但也有少数的线程安全的集合类,例如 Vector、Hashtable,它们都是非常古老的 API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。
对于这个包下非线程安全的集合,可以利用 Collections 工具类,该工具类提供的 synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。
从 JDK 1.5 开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。
- 第一类是以降低锁粒度来提高并发性能的容器,它们的类名以 Concurrent 开头,如 ConcurrentHashMap。
- 第二类是采用写时复制技术实现的并发容器,它们的类名以 CopyOnWrite 开头,如 CopyOnWriteArrayList。
- 第三类是采用 Lock 实现的阻塞队列,内部创建两个 Condition 分别用于生产者和消费者的等待,这些类都实现了 BlockingQueue 接口,如 ArrayBlockingQueue。
加分回答
Collections 还提供了如下三类方法来返回一个不可变的集合,这三类方法的参数是原有的集合对象,返回值是该集合的“只读”版本。通过 Collections 提供的三类方法,可以生成“只读”的 Collection 或 Map。
- emptyXxx():返回一个空的不可变的集合对象
- singletonXxx():返回一个只包含指定对象的不可变的集合对象
- unmodifiableXxx():返回指定集合对象的不可变视图
Java IO 面试题
BIO,NIO,AIO 有什么区别?
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
- NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO,异步 IO 的操作基于事件和回调机制。
详细回答
- BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (New I/O): NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了
Java 异常面试题
JVM 的异常处理
Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。