Java基础知识面试题

Java基础知识面试题

Java语言有啥特点?
简单易学
面向对象(封装、继承、多态、抽象)
跨平台(JVM实现跨平台)
可靠性
安全性
支持多线程
支持网络编程
编译和解析并存
JVM是什么?
JVM(Java虚拟机):是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现,目的是使用相同的字节码,它们都会给出相同的结果。
字节码:JVM可以理解的代码,扩展名为 .class 的文件。它不面向任何特定的处理器,只面向虚拟机。Java通过字节码的方式,在一定程度上解决了传统解析型语言执行效率低的问题,同时又保留了解析型语言可移植的特定。所以Java程序运行比较高效。
字节码和不同系统的JVM的实现是Java语言“一次编译,随处可运行”的关键。
Java 程序运行过程
OracleJDK 和 OpenJDK 的对比?
OpenJDK是一个参考模型并且是完全开源的;而OracleJDK是基于OpenJDK7构建的,并不是完全开源的。
OracleJDK比OpenJDK更稳定。两者代码几乎相同,但OracleJDK有更多的类和一些错误的修复。
在响应性和JVM性能方面,OracleJDK更出色一些。
import java 和 javax 有什么区别?
刚开始Java API所必需的包是java开头的包,javax是扩展API包来使用。
后来 javax 逐渐成为 Java API 的组成部分。
字符型常量 和 字符串常量的区别?
形式上:字符型常量是单引号引起的一个字符;字符串常量是双引号引起的0个或若干个字符;
含义上:字符型常量相当于一个整型值(ASCII值),可以参加表达式运算;字符串常量代表一个地址值(在内存中的存放位置);
内存大小:字符型常量只占2个字节;字符串常量占若干个字节;
标识符和关键字的区别?
标识符:是一个名字,类、变量、方法的名字都是标识符。
关键字:被Java语言赋予了特殊含义的标识符。例如:private / public / class / new 等。
Java泛型?类型擦除?常用的通配符?
Java泛型:JDK5引入的一个新特性,提供了编译时类型安全检测的机制。表现为:将类型当作参数传递给一个类或者方法。
泛型类
泛型接口
泛型方法
类型擦除:Java泛型是伪泛型,Java在编译期间,所有的泛型信息都会被擦除掉。
常用通配符
? 表示不确定的java类型
T 表示具体的一个java类型
K / V 分别表示Java键值对的 key value
E 表示 Element

== 和 equals 的区别?
== : 基本数据类型 比较的是值是否相等; 引用类型 比较的是内存地址是否一样(即两个对象是否同一个对象);
| 因为Java只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals:判断两个对象是否相等,不能用于比较基本数据类型的变量。
| equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。
类没有覆盖equals方法:等价于“==”,使用的默认是Object类的equals方法。
类覆盖了equals方法:一般都会覆盖equals方法,来比较两个对象的内容是否相等。
为什么重写equals方法时,必须重写hashCode方法?
hashCode()方法介绍
| 在Object类中的一个本地方法,作用是获取哈希码(散列码),它是一个int整数,这个哈希码的作用是确定该对象在哈希表中的索引位置。
为什么要有 hashCode()方法?
| 以“HashSet 如何检查重复”为例子说明。
| 当把对象添加到HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值做比较,如果没有相同的hashCode,则认为没有重复的对象。如果有相同的hashCode(发生碰撞),则会调用equals()方法来判断对象是否相同。如果相同,则不会让其加入成功。如果不同,则重新散列到其他位置。这样就减少使用equals的次数,提高了执行速度。
重写equals方法,必须重写hashCode方法
| hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()方法,则两个对象是永远不相等的(即使有同样的数据)。
如果两个对象相等(equals返回true),则它们的hashCode也一定相等。
如果两个对象的hashCode相等,但它们不一定相等(equals不一定返回true)。
为什么说Java中只有值传递?
按值调用(call by value):方法接收的是调用者提供的值。
按引用调用(call by reference):方法接收的是调用者提供的变量地址。
一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
Java才用按值调用,方法得到的是参数值的一个拷贝。
深拷贝、浅拷贝?
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递。
| 引用类型未新建对象,仍指向同一个对象(引用地址值)。
深拷贝:对基本数据类型进行值传递,对引用类型,创建一个新的对象,并复制其内容。
| 引用类型新建了对象,引用地址值不相等。
定义一个不做事且无参的构造函数的作用?
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则默认会调用父类中“无参构造方法”。因此,如果父类中没有定义“无参构造方法”,而子类构造方法又没有用super()去调用父类特定的构造方法,则编译会发生错误。
成员变量 与 局部变量 的区别?
成员变量属于类的;局部变量是在代码块或方法中定义的变量或方法的参数;
成员变量可以被private、public、static 等修饰符所修饰;局部变量不能被访问控制修饰符及static修饰;
成员变量和局部变量都可以被 final修饰;
成员变量存在于堆内存;局部变量存在于栈内存;
static修饰的成员变量属于类的,随着类的初始化而存在;没有static修饰的成员变量是对象的一部分,随着对象的创建而存在;局部变量随着方法的调用而存在和自动消失;
成员变量如果没有被赋初值,则会自动以类型的默认值赋值(被final修饰的例外,需要显示赋值);局部变量不会自动赋值;
对象实例和对象引用的关系?
对象实例存在堆内存中;对象引用存在栈内存中;
一个对象实例可以有若干个对象引用指向它;一个对象引用指向0个或1个对象实例;
类的构造方法?
作用是:完成对类对象的初始化工作。
一个类没有显示声明构造方法也可以执行,因为Java会给它一个默认的无参构造方法;如果显示声明了构造方法,那Java不会给它再添加默认的构造方法。
特性
名字与类名相同;
没有返回值,并且不能用void声明;
生成类的对象时自动执行,无需调用。
子类在构造方法里调用父类的无参构造方法的目的是?
帮助子类完成初始化工作。
面向对象的特征?
抽象
| 将一些事物的共性和相似点抽离出来,并将这些属性归为一个类。
封装
| 指把一个对象的状态信息(属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但可以通过对象提供的一些可以被外界访问的方法来操作属性。
继承
| 使用已存在的类(父类)作为基础建立新的类(子类),新类拥有父类的属性和方法,并可以增加新的属性和方法。通过继承,可以快速创建新类,提高代码的重用和程序的可维护性。
子类拥有父类的所有属性和方法(包括私有的),但无法访问父类私有的属性和方法,仅仅是拥有。
子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
子类可以覆盖父类的方法。
多态
| 一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
对象类型和引用类型之间,具有继承(类)或实现(接口)的关系;
引用类型变量发出的方法调用到底是哪个类中的方法,必须在运行期间才能确定;
多态不能调用“只在子类存在但父类不存在”的方法;
如果子类重写了父类的方法,执行的是子类覆盖的方法,否则是父类的方法;
String、StringBuffer、StringBuilder 的区别?
可变性
String类中使用final关键字修饰字符数组来保存字符串(private final char value[]),所以String对象是不可变的。
StringBuffer和StringBuilder都继承自AbstractStringBuilder类,在该类中也是使用字符数组(char[] value)来保存字符串,但没有用final关键字修饰,所以这两种对象都是可变的。
线程安全性
String 对象是不可变的,可以理解成常量,所以线程安全。
AbstractStringBuilder 是StringBuffer、StringBuilder的父类,定义了一些字符串的基本操作方法,如:append、indexOf等公共方法。StringBuffer对方法加了同步锁(public synchronized StringBuffer append(String str)),所以是线程安全的。StringBuilder没有对方法进行加同步锁,所以是非线程安全。
性能
String 类型进行改变的时候,会生成一个新的String对象,然后将指针指向新的String对象;
StringBuffer 进行改变时,会对StringBuffer对象本身进行操作;
StringBuilder与StringBuffer操作一样,但因为没有加同步锁,性能更好一些;
总结
操作少量的数据:适用 String
单线程操作字符串缓存区下大量的数据:适用 StringBuilder
多线程操作字符串缓存区下大量的数据:适用 StringBuffer
Object类常见的方法有哪些?
public final native Class getClass()
| 本地方法,返回当前运行时对象的Class对象,final关键字修饰,子类不可重写。
public native int hashCode()
| 本地方法,返回对象的哈希码,主要使用在哈希表中,如:HashMap。
public boolean equals(Object obj)
| 用于比较两个对象内存地址是否相等,String类对该方法重写后用于比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException
| 本地方法,用于创建并返回当前对象的一份拷贝。
public String toString()
| 返回 类名@实例的哈希码的16进制字符串。
public final native void notify()
| 本地方法,并且不能重写。唤醒一个在此对象监视器(锁)上等待的线程。如果有多个线程在等待,只会唤醒一个。
public final native void notifyAll()
| 跟notify()一样,区别在于会唤醒所有在此对象监视器等待的线程。
public final native void wait(long timeout) throws InterruptedException
| 暂停线程的执行。等待时间(timeout)到了会释放锁,sleep()方法没有释放锁。
public final void wait(long timeout, int nanos) throws InterruptedException
| nanos参数:表示额外时间(单位:纳秒,范围:0-999999, 1纳秒 = 十亿分之一秒)。超时的时间需要加上nanos纳秒(过去式)。查看源码,只要 nanos 在0-999999范围内,则 timeout++;
public final void wait() throws InterruptedException
| 暂停线程的执行,并且没有超时概念,一直等待。
protected void finalize() throws Throwable { }
| 实例被垃圾回收器回收的时候触发。
Java序列化中如果有些字段不想被序列化,怎么办?
对于不想被序列化的变量,使用 transient 关键字修饰。
transient 关键字:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
获取键盘输入常用的两种方法?
通过 Scanner 类
通过 BufferReader 类
反射机制?
什么是反射机制?
| 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息和动态调用对象的方法的功能称为反射机制。
静态编译 VS 动态编译
静态编译:编译时确定类型,绑定对象;
动态编译:运行时确定类型,绑定对象;
反射机制的优缺点
优点:运行期类型的判断、动态加载类、提高代码灵活度。
缺点
性能问题:反射相当于一系列解析操作,通知JVM要做的事情,比直接的java代码要慢很多。
安全问题:动态操作改变类的属性,增加了安全隐患。
反射技术的应用
JDBC连接数据库时使用Class.forName() 通过反射加载数据库的驱动程序;
Spring 的IOC创建对象(动态加载管理Bean)、AOP(动态代理)功能;
动态配置实例的属性;
获取Class 对象的两种方式
知道具体类的情况
| Class clazz = TargetObject.class;
通过 Class.forName() 传入类的路径获取;
| 一般是不知道具体类的,基本都是通过便利包下面的类来获取Class对象
| Class clazz = Class.forName(“com.xxx.TargetObject”);
Java异常有哪些?
所有的异常都有一个共同的祖先:java.lang.Throwable类,Throwable类有两个重要的子类Exception(异常)和Error(错误)。Exception 能被程序本身处理(try-catch),Error是无法处理的。
Exception : 程序本身可以处理的异常,可以通过 catch 捕获。
受检异常(必须处理的)
| 在编译过程中,如果没有被 catch / throw 处理,则无法通过编译。除了RuntimeException及其子类之外,其他的Exception类及其子类都属于受检异常。
IOException
ClassNotFoundException
SQLException
不受检异常(可以不处理)
| 在编译过程中,即使不处理也可以正常编译通过。RuntimeException及其子类统称为非受检异常。
NullPointException
NumberFormatException
ArrayIndexOutBoundsException
ArithmeticException
Error:程序无法处理的错误。JVM会选择终止线程。
Virtual MachineError (Java虚拟机运行错误)
OutOfMemoryError (虚拟机内存不够错误)
NoClassDefFoundError (类定义错误)
StackOverflowError (栈溢出错误)
try-catch-finally
try 块:用于捕获异常。其后可以接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
catch 块:用于处理try捕获到的异常。
finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当try块或catch块中遇到return语句时,finally块的语句将在方法返回之前被执行,并且finally语句返回值将覆盖原始返回值。
以下3种情况下,finally块不会被执行
在try或finally块中用 System.exit(int)退出程序。并且这句要在异常语句之前。
程序所在的线程死亡。
关闭CPU。
try-with-resources
适用范围:任何实现 java.lang.AutoCloseable 或 java.io.Closeable的对象。
面对需要关闭的资源,我们总是应该优先使用 try-with-resources,而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources 语句让我们更容易编写必须要关闭的资源代码,若采用try-finally 则几乎做不到这点。—《Effective Java》
InputStream、Scanner等资源都需要调用close()方法手动关闭。
try-catch-finally实现方式
使用Java 7 之后的try-with-resources 语句改造上面的代码:
当多个资源需要关闭的时候,通过使用分号分隔。
Java中IO流分为几种?
按照流的流向分:分为输入流和输出流;
按照流的操作单元分:分为字节流和字符流;
按照流的角色分:分为节点流和处理流;
Java IO流有40多个类,都是从以下4个抽象类基类中派生出来的。
InputStream / Reader :所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream / Writer :所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式
按操作对象
既然有了字节流,为什么还要有字符流?
不管是文件读写还是网络传输,信息的最小存储单元都是字节,那为什么I/O流操作要分为字节流和字符流?
字符流是由于Java虚拟机将字节转换得到的,这个过程比较复杂耗时,并且,如果不知道编码类型很容易出现乱码问题。所以,I/O流提供了一个直接操作字符的接口,方便对字符进行流操作。
怎么解决浮点数进度丢失?
使用BigDecimal
浮点数之间的等值判断,基本数据类型不能用“==”来比较,包装类型不能用“equals”来判断。(具体原理与浮点数的编码方式有关,精度丢失)
使用BigDecimal来定义浮点数,再进行运算操作
在使用BigDecimal时,为防止精度丢失,推荐使用它的BigDecimal(String) 构造方法或者BigDecimal.valueOf方法来创建对象,禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。
BigDecimal a = new BigDecimal(0.1f); 实际存储值为:0.10000000149
BigDecimal a = new BigDecimal(“0.1”); 正解
BigDecimal a = new BigDecimal.valueOf(0.1); 正解
工具类Arrays.asList() 使用注意事项?
Arrays.asList() 将数组转换为集合后,底层其实还是数组,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UpsupportedOperationException异常。
Arrays.asList() 的返回对象是一个Arrays 的内部类,并没有实现集合的修改方法。体现的是适配器模式,只是转换接口,后台的数据仍是数组。
传递的数组必须是对象数组,而不是基本类型。(Arrays.asList()是泛型方法,传入的对象必须是对象数组)
| 当传入一个基本类型的数组时,Arrays.asList()的得到的参数不是数组中的元素,而是数组本身。此时List的唯一元素就是这个数组。
如何将数组转换为ArrayList?
自定义方法实现
结合Arrays.asList()实现(推荐)
使用Java 8 的Stream(推荐)
使用Java 9 的List.of()方法
不要在 foreach 循环里进行元素的 remove / add 操作?
fail-fast(快速失败) 机制:java集合(Collection)中的一种错误机制。
当多个线程对同一个集合内容进行操作时,就可能会产生fail-fast事件。
| 例如:当某一个线程A通过iterator便利某集合的过程中,若该集合内容被其他线程所改变了,那么线程A访问该集合时,就会抛出ConcurrentModificationException异常。
单线程下,在foreach循环里调用集合类的remove方法,将抛出ConcurrentModificationException异常。
解决方案一:Java 8 开始可以使用Collection#removeIf()方法删除满足条件的元素。
解决方案二:使用Iterator方式。(如果并发,需要对Iterator对象加锁)
fail-safe(安全失败)机制:采用安全失败机制的容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所做的修改并不能被迭代器检测到,故不会抛ConcurrentModificationException异常。
创建对象的方式有哪几种?
new 方法。
clone():使用Object的clone方法。
反射
调用public无参构造函数,若没有,则会报异常。
| Object obj = clazz.newInstance();
调用带有参数的构造函数,先获取到其构造对象,再通过构造方法类获取实例。
| // 获取构造函数类的对象
| Constructor constructor = User.class.getConstructor(String.class);
| // 使用构造对象的newInstance方法初始化对象
| Object obj = constructor.newInstance(“name”);
发序列化创建对象。(被创建实例的类需实现Serializable接口)
如何实现静态代理?优缺点?
实现方式
1. 为现有的每一个类都编写一个对应的代理类,并且让它实现和目标类相同的接口。
2. 在创建对象时,通过构造器塞入一个目标对象,然后在代理对象的方法内部调用目标对象同名方法。
优点
在客户端和目标对象之间充当中介的作用,保护目标对象;
可以扩展目标对象的功能;(在调用目标对象方法的前后增加其他一些方法。)比如:打印日志
缺点
需要为每一个目标类编写对应的代理类,产生的类太多,工作量大。
相比直接调用目标对象的方法,效率低一些。
了解动态代理?在哪些地方用到?
作用
为其它对象提供一种代理以控制对这个对象的访问。
JDK动态代理的实现
在运行运行时,通过反射机制动态生成代理对象;
调用程序必须实现InvocationHandler接口;
使用Proxy类中的newProxyInstance方法动态的创建代理类。
在哪些地方应用到?
AOP、RPC 框架中都有用到。
JDK的动态代理与CGLIB的区别
JDK动态代理只能代理实现了接口的类;而CGLIB可以代理未实现任何接口的类。
JDK动态代理是通过反射的方式创建代理类;而CGLIB动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能将代理类声明为final类型的类和方法;
JDK动态代理的效率更优。
对Java注解的理解,解决声明问题?
Java语言中的类、方法、变量、参数和包等都可以注解标记,在程序运行期间可以获取到相应的注解以及注解中定义的内容。
注解极大地简化了代码,通过注解可以帮助我们简单地完成一些事情;比如:Spring中如果检测到你的类被 @Component 注解标记的话,Spring容器在启动的时候就会把这个类进行管理,我们可以通过 @Autowired 注解注入类的实例。
内存泄漏和内存溢出?
内存泄漏
定义:是指不再使用的对象持续占用内存或者它们占用的内存得不到及时释放,从而造成内存空间的浪费。
根本原因:长生命周期的对象持有短生命周期对象的引用;
内存泄漏场景
静态集合类引起:静态成员的生命周期是整个程序运行期间。
| 例如Map是在堆上动态分配对象,正常情况下使用完毕后,就会被gc回收。而如果Map被static修饰,且没有删除机制,静态成员是不会被回收的,所以导致很大的Map一直停留在堆内存中。懒初始化static变量,尽量避免使用。
当集合里的对象属性被修改后,再调用remove()方法是不起作用的。
| 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了。
各种连接对象(IO流对象、数据库连接对象、网络连接对象)使用后未关闭。
| 因为每个流在操作系统层面都对应了打开的文件句柄,流没有关闭,会导致操作系统的文件句柄一直处于打开状态,而JVM会消耗内存来跟踪操作系统打开的文件句柄。
监听器的使用。
| 在释放对象的同时,没有相应删除监听器的时候,也可能导致内存泄露。
不正确使用单例模式。
| 单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么被持有的对象将不能被JVM正常回收。
解决措施
尽量减少使用静态变量,类的静态变量的生命周期是和类同步的。
声明对象引用之前,明确内存对象的有效作用域,尽量减小对象的作用域,将类的成员变量改写为方法内 的局部变量。
减少长生命周期的对象持有短生命周期的引用。
使用StringBuilder和StringBuffer替换String进行字符串连接。避免产生大量临时字符串。
对于不需要使用的对象,手动设置null值,不管GC何时会开始清理,我们都应该及时的将无用的对象标记为可被清理的对象。
各种连接(数据库连接、网络连接、IO连接)操作,操作结束都务必显式调用close关闭。
内存溢出
指程序运行过程中无法申请到足够的内存而导致的一种错误。
通常发生在OLD段或perm段垃圾回收后,仍然无内存空间容纳新的对象的情况。
内存溢出场景
JVM Heap(堆)溢出:(java.lang.OutOfMemoryError:java heap space)
| 发生这种问题的原因是java虚拟机创建的对象太多了,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了。

解决方法
| JVM在启动的时候,会自动设置JVM Heap的值,可以利用JVM提供的 -Xmn -Xms -Xmx 等选项进展设置。Heap的大小是新生代和老年代之和。
手动设置JVM Heap的大小;
检查程序,看是否有死循环或不必要地创建大量对象;
Metaspace溢出:(java.lang.OutOfMemoryError:Metaspace)
| 原因:程序中使用了大量的jar或class,使java虚拟机装载类的空间不够。

解决方法
| ​方法区用于存放java类型的相关信息。在类装载器加载class文件到内存的过程中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。当需要存储信息而方法区的内存占用又已经达到 -XX:MaxMetaspaceSize设置的最大值,将会抛出此异常。测试基本思路:运行时产生大量的类去填满方法区,直到溢出。
通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 设置永久代大小即可。
栈溢出:(java.lang.OutOfMemoryError:Thread Stack space)
| 线程的方法嵌套调用层次太多(如:递归调用),以致把栈区溢出了。
解决方法
修改程序;
通过 -Xss:来设置每个线程的Stack 大小;
BIO、NIO、AIO
BIO (Blocking I/O)
| 同步阻塞I/O模式,数据的读写必须阻塞在一个线程内等待其完成。
NIO (Non-Blocking/New I/O)
| 是一种同步非阻塞的I/O模型,对应java.nio包,提供了Channel、Selector、Buffer 等抽象。
| 单线程从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。非阻塞写数据也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程可以同时去做别的事情。JDK的NIO底层由epoll实现。
NIO中所有I/O操作都是从Channel(通道)开始的。
从通道进行数据读取,创建一个缓存区,然后请求通道读取数据。
从通道进行数据写入,创建一个缓冲区,填充数据,并请求通道写入数据。
AIO (Asynchronous I/O)
| 异步非阻塞I/O模型,异步I/O是基于事件和回调机制实现的。应用操作之后会直接返回,不会阻塞,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
Java中finalize()方法的使用?
finalize() 是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
finalize() 方法中一般用于释放非Java资源(如:打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存。
避免使用的原因
finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间是任意长的。我们不能依赖finalize()方法能及时的回收占用的资源,可能在资源耗尽之前,gc仍为触发。因此通常的做法是提供显式的close()方法供客户端手动调用。
finalize() 方法意味着回收对象时需要进行更多的操作,从而延长了对象回收的时间。
Java中Class.forName 和 ClassLoader的区别?
Class.forName() 和 ClassLoader 都可以对类进行加载;
ClassLoader 遵循双亲委派模型,实现的功能是通过一个类的全限定名来获取描述此类的二进制字节流,获取到二进制流后放到JVM中。ClassLoader只做一件事,就是将.class文件加载到JVM中,不会只想static中的内容。
Class.forName()方法实际上也是调用ClassLoader来实现的,不同的是除了将类的.class文件加载到JVM中之外,还会对类进行初始化,执行类中的static块。
讲一下CopyOnWriteArrayList和CopyOnWriteArraySet?
CopyOnWrite 容器:写时复制的容器。往一个容器添加元素时,不是直接往当前容器添加,而是先将当前容器进行copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
添加元素的时候需要加锁,否则多线程写的时候会copy出N个副本
读的时候不需要加锁,多线程读写时,读到的可能还是旧数据,因为读的时候不会对当前容器加锁
CopyOnWrite 并发容器主要用于读多写少的并发场景。
优点:可以对CopyOnWrite容器进行并发读,而不需要加锁,因为当前容器不会添加任何元素。
| CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。
缺点
内存占用问题
| 因为CopyOnWrite是写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。如果这些对象占用的内存比较大,那么有可能会造成频繁的Yong GC 和 Full GC。
针对内存占用问题,可以通过压缩容器中的元素来减少大对象的内存消耗,如元素全是10进制的数字,可考虑把它压缩成36进制或者64进制。或者不使用CopyOnWrite容器,而使用其他并发容器,如:ConcurrentHashMap。
数据一致性问题
| CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
HashMap了解多少?
HashMap底层实现
HashMap 是用数组 + 链表 + 红黑树(JDK1.8开始增加了红黑树)进行实现的,当添加一个元素(key-value)时,首先计算元素key的hash值,并根据hash值来确定插入数组的位置,如果发生碰撞(存在其他元素已经被放在数组同一位置),这个时候便使用链表来解决哈希冲突,当链表长度太长的时候,便将链表转为红黑树来提高搜索的效率。
数组的容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算。
数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组,这个0.75就是默认的负载因子,可由构造函数传入。
为了解决碰撞,数组中的元素是单向链表类型。当链表长度达一个阈值时(>=8),会将链表转换为红黑树提高性能。而当链表长度缩小到另一个阈值(<=6)时,又会将红黑树转换回单向链表提高性能。
HashMap的put方法执行流程/步骤
1. 判断数组table是否为null,若为null则执行resize()扩容操作。
2. 根据键key的值计算hash值得到插入的数组索引i,若table[i] == nulll,则直接新建节点插入,进入步骤6;若table[i]非null,则继续执行下一步。
3. 判断table[i]的首个元素key是否和当前key相同(hashCode和equals均相同),若相同则直接覆盖value,进入步骤6,反之继续执行下一步。
4. 判断table[i]是否为treeNode,若是红黑树,则直接在树中插入键值对并进入步骤6,反之继续执行下一步。
5. 遍历table[i],判断链表长度是否大于8,若>8,则把链表转换为红黑树,在红黑树中执行插入操作;若<8,则进行链表的插入操作;遍历过程中若发现key已存在则会直接覆盖该key的value值。
6. 插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,若超过则进行扩容。
HashMap的get方法执行流程/步骤
1. 首先定位到键所在的数组的下标,并获取对应节点n。
2. 判断n是否为null,若n为null,则返回null并结束;反之,继续下一步。
3. 判断n的key和要查找的key是否相同(key相同指的是hashCode和equals均相同),若相同则返回n并结束;反之,继续下一步。
4. 判断是否有后续节点m,若没有则结束;反之,继续下一步。
5. 判断m是否为红黑树,若为红黑树则遍历红黑树,在遍历过程中如果存在某一个节点的key与要找的key相同,则返回该节点;反之,返回null;若非红黑树则继续下一步。
6. 遍历链表,若存在某一个节点的key与要找的key相同,则返回该节点;反之,返回null。
HashMap的扩容机制
扩容是为了防止HashMap中的元素个数超过了阀值,从而影响性能所服务的。而数组是无法自动扩容的,HashMap的扩容是申请一个容量为原数组大小两倍的新数组,然后遍历旧数组,重新计算每个元素的索引位置,并复制到新数组中;又因为HashMap的哈希桶数组大小总是为2的幂次方,所以重新计算后的索引位置要么在原来位置不变,要么就是“原位置+旧数组长度”。其中,threshold和loadFactor两个属性决定着是否扩容。threshold=LengthloadFactor,Length表示table数组的长度(默认值为16),loadFactor为负载因子(默认值为0.75);阀值threshold表示当table数组中存储的元素个数超过该阀值时,即需要扩容。
HashMap的扩容使用新的数组代替旧数组,然后将旧数组中的元素重新计算索引位置并放到新数组中,对旧数组中的元素如何重新映射到新数组中?
HashMap的哈希算法数组扩容
| (a)为扩容前,key1和key2两个key确定索引的位置;(b)为扩容后,key1和key2两个key确定索引的位置;hash1和hash2分别是key1与key2对应的哈希“与高位运算”结果。
| (a)中数组的高位bit为“1111”,1
20 + 121 + 122 + 123 = 15,而 n-1 =15,所以扩容前table的长度n为16;
| (b)中n扩大为原来的两倍,其数组大小的高位bit为“1 1111”,1
20 + 121 + 122 + 123 + 124 = 15+16=31,而 n-1=31,所以扩容后table的长度n为32;
| (a)中的n为16,(b)中扩大两倍n为32,相当于(n-1)这部分的高位多了一个1,然后和原hash码作与操作,最后元素在新数组中映射的位置要么不变,要么向后移动16个位置。
HashMap中数组扩容两倍后位置的变化
| 因此,在扩充HashMap,复制数组元素及确定索引位置时不需要重新计算hash值,只需要判断原来的hash值新增的那个bit是1,还是0;若为0,则索引未改变;若为1,则索引变为“原索引+oldCap”
HashMap中数组16扩容至32
| JDK1.7中扩容时,旧链表迁移到新链表的时候,若出现在新链表的数组索引位置相同情况,则链表元素会倒置,但从上图中看出JKD1.8的扩容并不会颠倒相同索引的链表元素。
扩容机制设计的优点
1. 省去了重新计算hash值的时间(由于位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快),只需判断新增的一位是0或1;
2. 由于新增的1位可以认为是随机的0或1,因此扩容过程中会均匀的把之前有冲突的节点分散到新的位置(bucket槽),并且位置的先后顺序不会颠倒;
3. JDK1.7中扩容时,旧链表迁移到新链表的时候,若出现在新链表的数组索引位置相同情况,则链表元素会倒置,但JKD1.8的扩容并不会颠倒相同索引的链表元素。
HashMap 和 Hashtable 的区别
线程安全
HashMap是非线程安全的;
Hashtable是线程安全的,方法被synchronized修饰;
是否允许NULL值
HashMap允许有一个key是NULL,允许值为NULL;
Hashtable无论是key还是value都不允许为NULL;
继承的父类
HashMap和Hashtable都实现了Map接口;
HashMap继承的父类是AbstractMap;
Hashtable继承的父类是Dictionary;
contains()方法
HashMap没有contains()方法,但有containsValue和containsKey方法;
Hashtable保留了contains方法,也有containsValue和containsKey方法;contains方法与containsValue方法效果一样(containsValue方法里调用contains方法)。
计算hash值的方式不同
二叉树
某节点的左子树节点值仅包含小于该节点值
某节点的右子树节点值仅包含大于该节点值
左右子树每个也必须是二叉查找树
图示
红黑树
每个节点都有红色或黑色
树的根始终是黑色的
没有两个相邻的红色节点(红色节点不能有红色父节点或红色子节点,并没有说不能出现连续的黑色节点)
从节点(包括根)到其任何后代NULL节点(叶子结点下方挂的两个空节点,并且认为他们是黑色的)的每条路径都具有相同数量的黑色节点。
ConcurrentHashMap的底层实现?
底层数据结构
JDK 1.7 的ConcurrentHashMap底层采用 分段的数组+链表 实现;
JDK 1.8 采用的数据结构跟HashMap 1.8 一致:数组 + 链表 | 红黑二叉树;
实现线程安全的方式
JDK 1.7 ,ConcurrentHashMap(分段锁)对整个桶数据进行了分割分段(Segment),每一把锁只锁容器其中一部分的数据,多线程访问容器中不同数据段的数据,就不会存在锁竞争,提高并发访问率;
JDK 1.8,开始摒弃Segment的概念,并发控制使用 synchronized 和 CAS 来操作,像是优化过且线程安全的HashMap。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率非常高。
ConcurrentHashMap的读操作为什么不需要加锁?
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的,和数组用volatile修饰无关;
数组用volatile修饰主要是保证在数组扩容的时候保证可见性;
总结:定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,而不会读到过期值。由于get操作只需读不需要写共享变量,所以不用加锁。之所以不会读到过期值,依据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,get总能拿到最新值。
HashMap、LinkedHashMap、TreeMap 有什么区别?各自的使用场景?
LinkedHashMap 保存了记录的插入顺序,在用Iterator遍历时,先取到的记录是先插入的,遍历比HashMap慢;
TreeMap 实现SortMap接口,能够把它保存的记录根据键排序(默认按键值升序,也可以指定排序的比较器)。
使用场景
HashMap:一般情况下,使用最多的;适用于Map的插入、删除和定位元素;
TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下;
HashMap 多线程操作死循环问题
由于多线程并发下进行扩容(调用rehash()方法)造成元素之间形成一个循环链表。
正常Rehash过程
| 1. 假设了hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
| 2. 最上面的是old hash表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table1这里了。
| 3. 接下来的三个步骤是Hash表 resize成4,然后所有的元素重新rehash的过程。
并发的Rehash过程
| 假设有两个线程:线程一和线程二,假设出现以下过程:
| 1. 线程一执行到 next = e.next; 的时候,被CPU挂起; (当前元素e = 3, next = 7;)
| 2. 线程二执行完成,完成了扩容操作,链表被改变,元素被翻转;(元素 7 的next元素变为 3)
| 3. 线程一重新获得CPU,继续执行,由于链表已经被线程二改了,原来元素3的next元素是7,现在发现7的next元素居然是3,一脸闷逼,进入死循环疯狂操作!!!
JDK 1.8 已经解决了该问题,但是由于HashMap是非线程安全的,多线程下使用还是会存在其他问题,比如:数据丢失。所以多线程下如果需要更新操作的,建议改用 ConcurrentHashMap。

发布于

2022-08-14

更新于

2026-04-28

许可协议

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...