目录
9. try catch finall,try里面有return,还会执行finally吗?
10.OOM(内存超限)你遇到过那些情况?SOF(堆栈溢出)你遇到过那些情况?
12.为什么重写 equals() 时必须重写 hashCode() 方法?
1.泛型中extends和super的区别?
extends表示包括在T在内的任何T的子类
super表示包括T在内的任何T的父类
Java中的泛型实际上是假的泛型,它只发生在编译期间
何以见得?看下述代码:
List<Integer> a = new ArrayList<>();
List<Boolean> b = new ArrayList<>();
System.out.println(a.getClass().equals(b.getClass()));
这段代码最终的结果是true,这意味着List并没有把泛型的信息存储到Class中,这也就代表了泛型其实只发生在编译期间。
2.instanceof关键字的作用?
instanceof严格来说是Java中一个双目运算符,用来测试一个对象是否为一个类的实例,用法用:
boolean b = obj instanceof Class
其中obj是一个对象,而Class表示一个类或者是一个接口,当obj为Class对象,或者是其直接或间接子类,或者是其接口的实现类,都会返回true,否则返回false。
注意,如果obj为null,则返回false。
3.hashCode的作用?
可以提高集合中查找元素的效率,当集合要添加新的元素的时候,先调用这个对象的hashCode方法,就能定位到它应该放置的物理位置,如果这个位置上没有元素,则直接存到这个位置上;如果这个位置上有元素,则调用它的equals方法来判断,相同的话就不存。
也可以用来做一些提前的判断,比如:
- 如果两个对象的hashCode值不相同,那么这两个对象一定不是同一个对象
- 如果两个对象的hashCode值相同,则它们有可能是一个对象,也有可能不是一个对象
- 如果两个对象相同,那么它们的hashCode值一定相同
4.Java的四种引用?
强引用:Java中默认使用的是强引用,也是平时用的最多的一种引用,在OOM的时候也不会被回收,只有当对象明确的被设置为null时,才会被回收,一般指的是用new关键字创建的对象。
软引用:用于描述一些有用但是非必需的对象,当OOM时,JVM会回收软引用指向的对象内存以释放空间。
弱引用:用于描述一些非必要的对象,被弱引用指向的对象只能存活到下一次垃圾回收之前。(或者说只要垃圾回收器发现了它,就会回收)
虚引用:是最弱的一种引用方式,一个对象是否有虚引用和该对象的生命周期没有任何的关系,也无法通过虚引用来获取对象实例。虚引用主要用于对象被回收时获得一个系统通知。
5.Java有哪些方式来创建对象?
1.使用new关键字来创建对象。
2.使用反射机制来创建对象,可以在运行时动态的创建对象。
3.使用clone方法来创建对象,可以创建一个与原始对象相同的新对象。
4.使用反序列化创建对象。
5.使用工厂方法来创建对象。
public static MyObject createMyObject() {
return new MyObject();
}
MyObject obj = createMyObject();
6.static都有哪些用法?
基本都知道static的两个用法:静态变量和静态方法。(静态方法在(一)中15,有讲解)
除了静态变量和静态方法之外,还可以用于静态代码块,或者是用来修饰内部类,被称为静态内部类,最后一种用法就是静态导包,用静态导包的方法导入的资源,在使用的时候可以不需要类名。例如:
//System.out.println(Math.sin(20));传统做法
System.out.println(sin(20));
7. 3*0.1==0.3的结果是什么?
false。因为浮点数不能完全精确的表示出来。
8. a=a+b与a+=b有什么区别?
a = a + b 表示将a和b相加的结果赋值给a,这个过程中会创建一个新的对象来存储相加之后的结果,然后将a的引用指向新的对象。
a += b 则是一个组合复制运算符,不会创建一个新对象,而是在a原有的基础上进行加法操作。
+=操作符会进行隐式的自动类型转换。例如:
short s1= 1;
s1 = s1 + 1; //错误写法
s1 += 1;
在s1 = s1 + 1时,会自动提升为int类型,导致s1 + 1的结果的类型是int类型而不是short类型,所以会导致报错。
+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错。
9. try catch finall,try里面有return,还会执行finally吗?
会执行,并且finally的执行早于try里面的return。
finally块中的代码会在try块中的代码执行完毕后执行,并且在方法返回之前执行。也就是说,如果try中有return,那么在return之前,finally中的代码都会被执行。
注意:
- 不管有没有异常,finally中的代码都会执行。
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch里面保存的返回值
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
例如上面代码会返回3。
10.OOM(内存超限)你遇到过那些情况?SOF(堆栈溢出)你遇到过那些情况?
OOM:
- Java Heap(堆)溢出。Java堆用来存储对象实例,如果一直用new,也就是强引用的方式来创建对象,就会导致对象数量达到最大堆容量之后出现OOM。
出现这种异常,一般手段是先通过内存映像分析工具分析,先确定原因是内存泄漏还是内存溢出。
如果是内存泄露,可以进一步查看泄露对象到GCRoots的引用链,就能找到泄露对象是怎么导致垃圾收集器无法自动回收的。
如果是内存溢出,就应该检查JVM的参数的设置是否适当。例如增加 JVM 的堆内存大小或调整垃圾回收器的参数。此外,还可以通过减少程序中对象的创建和持有来降低内存使用量,从而避免出现堆溢出异常。
-
虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度(通常是方法递归调用过深),将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的空间,将抛出OutOfMemoryError异常。
需要注意的是,栈的大小越大则可分配的线程数就越少。
- 运行时常量池溢出
- 方法区溢出
方法区用于存放 Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的 class 对象没有被及时回收掉或者 class信息占用的内存超过了我们配置。方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量 Class 的应用中,要特别注意这点。
SOF:
StackOverflflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。因为栈一般默认为 1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过 1m 而导致溢出。栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、 List 、 map 数据过大。
11.hashCode()与equals()之间的关系?
在Java中,每个对象都可以调用自己的hashCode()方法来得到自己的hashCode值,相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,虽然在java中做不到那么绝对,但是我们依然可以用hashCode值做一些提前的判断,比如:
- 如果两个对象的hashCode值不相同,那么这两个对象一定不是同一个对象
- 如果两个对象的hashCode值相同,则它们有可能是一个对象,也有可能不是一个对象
- 如果两个对象相同,那么它们的hashCode值一定相同
在Java一些集合类的实现中,比较两个对象是否相等,就会根据上面的规则来进行,先调用hashCode()方法得到hashCode值来进行比较,如果hashCode值相同,则在进一步调用equals()方法进行比较,一般来说equals()方法的实现比较复杂,而hashCode()方法的实现比较容易,所以会先用hashCode值进行一个比较。
这也是为什么重写equals()就必须重写hashCode()方法的原因。
12.为什么重写 equals() 时必须重写 hashCode() 方法?
在上面,我说过两个相等的对象,他们的hashCode()的值也是相等的,意思就是,如果他们equals()的值相等,那么hashCode()的值也要必要相等。这是需要重写hashCode()方法的直接原因。下面我来深度解析为什么:
在集合框架中,对于哈希表的实现(如HashMap、HashSet等),是根据对象的哈希码值来进行查找和存储的。如果equals()与hashCode()的实现不一致,那么在查找过程中,两个equals()返回true的对象可能会被认为是不同的对象,因为它们的哈希码值不同。这就会导致对对象进行查找或删除时出现问题。
举个例子,对象A和B的equals()方法返回true,但它们的hashCode()方法返回的哈希码值不同。当我们将A和B作为键值存入HashMap中时,HashMap会根据它们的哈希码值生成两个不同的索引位置,因此在查询Hashmap中是否包含键为A的元素时,将无法找到A,进而导致出错。
因此,要保证对象的equals()和hashCode()方法实现一致性,通常的做法是如果两个对象的equals()方法返回true,则它们的hashCode()方法返回的哈希码值也相等。这样,即使在哈希表查找时,两个相等的对象也可以通过相同的索引位置进行查找,而不会出现查找失败的情况。
13.浅拷贝和深拷贝的区别?
在这里,我先分别讲述浅拷贝和深拷贝是什么,再谈区别:
浅拷贝:
浅拷贝会复制原始对象的引用,但不会复制对象本身,也就是说,被拷贝对象的所有成员变量都会指向同一个原始对象,而没有创建新的对象。因此,对拷贝对象中的成员变量的修改也会影响到原始对象。
在Java中,可以通过Object类的clone()方法实现浅拷贝,也可以通过构造方法实现,下面是代码示例:
class Person implements Cloneable {
public String name;
public int age;
public Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Address {
public String country;
}
public class ShallowCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person();
person1.name = "Tom";
person1.age = 18;
person1.address = new Address();
person1.address.country = "USA";
Person person2 = (Person) person1.clone();
System.out.println(person1.address == person2.address); // true
}
}
上述代码中,通过重写clone()方法实现了浅拷贝,可以看到person1和person2都指向了同一个Address对象。
深拷贝:
深拷贝会创建一个新的对象,并且会复制原始对象中的所有成员变量,也就是说它会递归地拷贝对象中的所有成员对象。因此,对拷贝对象中的成员变量进行的修改不会影响原始对象。
在Java中,可以通过序列化和反序列化实现深拷贝,也可以通过手动编写拷贝方法实现,代码示例如下:
class Person implements Serializable {
public String name;
public int age;
public Address address;
// 构造方法
public Person() {
}
/**
* Person对象的深拷贝方法
*
* @return 返回一个全新的Person对象
* @throws IOException
* @throws ClassNotFoundException
*/
public Person deepClone() throws IOException, ClassNotFoundException {
// 创建一个字节输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 创建一个对象输出流,并将对象写入到输出流中
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 从字节输出流中读取字节数组,并使用字节数组创建一个字节输入流
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
// 创建一个对象输入流,从字节输入流读取对象,并进行反序列化
ObjectInputStream ois = new ObjectInputStream(bis);
// 返回反序列化后的新对象
return (Person) ois.readObject();
}
}
class Address implements Serializable {
public String country;
}
public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
// 创建Person对象
Person person1 = new Person();
person1.name = "Tom";
person1.age = 18;
person1.address = new Address();
person1.address.country = "USA";
// 深拷贝
Person person2 = person1.deepClone();
System.out.println(person1.address == person2.address); // false
}
}
上述代码中,通过手动实现深拷贝的方式,通过序列化和反序列化创建了全新的对象,person1和person2的Address对象不是同一个,修改person2.address对象不会对person1.address对象造成影响。
通过以上两个demo的举例,相信大家都有了自己的理解,下面我做出我自己总结的区别:
-
产生的对象不同:浅拷贝会复制原始对象的引用,深拷贝会创建一个新的对象。
-
内容不同:浅拷贝复制的是原始对象中基本类型的值和对象的引用,它并不会复制引用对象本身。深拷贝会同时复制引用对象本身。
-
对原始对象的影响:浅拷贝会在原始对象中产生影响,而深拷贝则不会。
-
复制方式不同:浅拷贝可以通过直接赋值或clone()方法实现,而深拷贝则需要通过序列化和反序列化、手动编写拷贝方法等方式实现。
-
复制速度不同:浅拷贝通常比深拷贝速度快,因为它只是复制原始对象中的引用,而不需要创建新的对象进行复制。深拷贝需要递归地复制对象,速度较慢。
总结: 选用浅拷贝还是深拷贝,应该根据具体的情况来考虑。例如,如果被复制的对象比较简单,且不会修改,那么使用浅拷贝可以大大提升效率。但如果被复制的对象比较复杂,或者可能被修改,那么使用深拷贝更为安全。
14.请你介绍一下SPI和API是什么?他们有什么区别?
SPI是指服务提供商接口,其定义方式与API类似,但用于不同的目的。SPI是一种Java中的机制,用于定义和实现某个服务提供者的规范,这样其他模块可以通过配置文件或其他方式来动态地替换该服务提供者的实现。
API是指应用程序编程接口,是一组软件接口,定义了不同软件组件之间交互的方法。API通常由一组库或者例程组成,开发人员可以利用这些库和例程以进行软件开发。API可以与软件外部的其他应用程序进行交互,并允许应用程序之间共享数据。
API和SPI的主要区别在于它们用于不同的目的。API主要用于软件组件之间的交互和数据共享,而SPI主要用于提供服务的规范定义和实现方式。
简单来说:API是应用程序编程接口的缩写,主要是用来帮助不同的软件组件之间接口的交互和数据的共享。SPI是服务提供商接口的缩写,主要是用来定义和实现一种服务的规范,并通过配置文件或其他方式来动态地替换其实现方式。
15.你了解BIO、NIO、AIO吗?他们有什么区别?
这三种技术都属于Java NIO(new I/O)框架中的一部分,用于提高Java代码的性能、效率。
BIO:是Blocking I/O的缩写,是最传统的I/O模型。在BIO中,I/O操作将阻塞当前线程,直到数据读取完毕。这种模型适用于相对少量的连接和数,但不适合高并发环境,因为每个连接都需要由一个线程单独处理。
NIO:是Non-blocking I/O的缩写,它引入了选择器和通道的概念,可以实现一次线程处理多个连接的情况。在NIO中,读取和写入不再是阻塞的,而是异步非阻塞的。这使得NIO能够比BIO更有效处理大量并发的连接和数据。
AIO:是Asynchronous I/O的缩写,是在NIO的基础之上发展而来的,异步I/O的特点是数据读取和处理是异步的,处理器在读取和处理数据时可以继续去处理其他数据,不需要等待 I/O 操作完成。AIO更加适用于处理I/O密集型的任务。
因此,BIO适用于低延迟、低负载的环境,NIO适用于需要处理大量连接和数据的高并发、高吞吐量的环境,而AIO则更适合处理一些 I/O 密集型任务。
16.char型变量能否存储一个中文汉字?为什么?
首先,答案是肯定的。
在Java中,一个char类型变量占了2个字节,也就是16位,而Java默认采用Unicode编码,一个Unicode码占了16位,也就是两个字节。
一个汉字或者一个英文字母,都是一个Unicode码存储的,所以一个char可以存一个中文汉字。
17.什么是引用类型?
引用类型声明的变量是指该变量在内存中实际存储的是一个地址,而非一个值,这个变量它的实体在堆中。
引用类型如:接口、数据、类等。
18.说说你对this关键字的理解?
在Java中,this关键字代表当前对象的引用。它可以用来访问当前对象中的属性和方法,并且也可以用来构造函数中引用当前的对象。
具体来说,当一个类中有多个同名的变量或者方法时,使用this关键字可以明确指出当前对象中需要操作的变量或者方法。在构造函数中,可以使用this关键字来调用当前类中的其他构造函数,以方便代码的重用。此外,this关键字还可以用于方法链式调用,即在一个方法中返回当前对象的引用,从而可以在连续调用多个方法时更加简便。
19.Java的类加载机制是什么?
Java类加载机制是指在运行时加载Java类的过程,它主要分为三个步骤:加载、连接和初始化。
-
加载:查找并加载类的二进制数据。在这个阶段,Java虚拟机会根据类名找到对应的二进制数据,将其读入内存,并生成一个对应的java.lang.Class对象保存二进制数据。
-
连接:验证、准备和解析类的二进制数据。连接阶段主要完成的工作包括:对类的二进制数据进行格式和语义检查,为静态变量分配内存并设置初始值,解析符号引用为直接引用并生成对应的二进制代码等。
-
初始化:给类的静态变量赋值,并执行静态代码块。初始化阶段是类加载机制的最后一个阶段,它的主要任务是执行类的静态变量赋值与静态代码块的初始化操作。
Java类加载机制的特点包括以下几点:
-
类的加载是按需加载的,即在初始化阶段才会加载类的二进制代码。
-
类的加载是按照指定的顺序进行的,即先加载父类,再加载子类。
-
类的加载遵循双亲委派机制,即由上至下依次在父子类加载器之间进行类的查找和加载。通过双亲委派机制可以保证类的安全性和可靠性。
-
Java中的类加载器有三种:启动类加载器、扩展类加载器和应用程序类加载器。它们分别负责加载不同的类。
总之,Java类加载机制是Java程序运行的基础,它能够保证Java程序能够在运行时动态地加载和卸载类,从而实现程序的灵活性和可扩展性。
20.Java中的单例模型指的是什么?
Java中的单例模式是一种常见的设计模式,它的主要作用是保证系统中只有一个实例对象,并提供全局的访问点。
在Java中实现单例模式有多种方式,其中比较经典的方式包括:懒汉式单例、饿汉式单例、双重检查锁定单例、静态内部类单例等。不同的实现方式有不同的特点和适用场景,这里以懒汉式单例和饿汉式单例为例进行介绍。
懒汉式单例:指在首次使用的时候才进行对象的创建。在多线程环境下,需要考虑线程安全问题。懒汉式单例的核心代码如下:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上述代码通过将构造函数定义为私有的,然后提供一个静态的getInstance()方法返回唯一的实例对象,实现了懒汉式单例模式。其中的synchronized关键字保证了多线程下只有一个线程能够访问,确保线程安全。
饿汉式单例:指在类加载的时候就进行对象的创建,因此不存在线程安全问题。饿汉式单例的核心代码如下:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
上述代码通过将构造函数定义为私有的,并在类加载的时候就创建了唯一的实例对象,实现了饿汉式单例模式。由于是在类加载的时候即创建,因此不存在线程安全问题。
总之,单例模式是一种非常常用的设计模式,它可以避免一个类在系统中被创建多次,从而保证系统的稳定性和性能。同时,单例模式的实现方式也可以根据具体的需求而异,例如懒汉式单例、饿汉式单例、双重检查锁定单例、静态内部类单例等。