首页 > 资源分享 > 【面试题整理】36个经典面试题,来测一测您的Java基础

【面试题整理】36个经典面试题,来测一测您的Java基础

头像
牛客881515734号
编辑于 2018-11-10 09:52:55 APP内打开
赞 19 | 收藏 220 | 回复7 | 浏览5540

Java基础面试自测提纲

  1. 面向对象的特征有哪些?
  2. 谈谈对Java中多态的理解?
  3. 项目中哪些地方使用过多态?
  4. Java有哪些访问修饰符?
  5. 项目中的哪些地方使用过protected修饰符?
  6. Java有哪几种基本数据类型?
  7. 基本数据类型之间是怎么转换的?
  8. Integer类型值的缓存机制是怎样的?
  9. Java中重写与重载的区别是什么?
  10. 谈谈对static关键字的理解?
  11. Java中变量的初始化顺序是怎样的?
  12. 谈谈对final关键字的理解?
  13. 如何初始化final所修饰的成员变量?
  14. 接口与抽象类有什么区别?
  15. 接口与抽象类分别在什么场景下使用?
  16. 谈谈对内部类的理解?
  17. 静态内部类和非静态内部类有什么区别?
  18. Object类有哪些方法?
  19. 重写equals方法的原因、方式和注意事项
  20. 重写hashCode方法的原因、方式和注意事项
  21. Class类是什么?
  22. 如何获取Class类的实例?
  23. Class类的实例可以做什么?
  24. 深拷贝和浅拷贝的区别和具体实现
  25. 谈谈对反射和动态***的理解?
  26. String、StringBuffer与StringBuilder的区别
  27. Exception与Error有何异同?
  28. 编译时异常和运行时异常的区别
  29. 常见的运行时异常有哪些?
  30. final、finally与finalize的区别
  31. 什么是Java的序列化和反序列化?
  32. 如何实现Java中对象的序列化?
  33. Java中四种引用的基本概念、具体实现、声明周期和应用场景
  34. 什么是同步IO和异步IO、阻塞IO和非阻塞IO?
  35. BIO、NIO与AIO的概念与区别
  36. Lambda表达式的作用和基本语法

1. 面向对象的特征有哪些?

  • 抽象:将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面,分别对应于类的属性和方法。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
  • 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。总结:封装主要体现在两个方面,类是对数据和数据操作的封装,类中的方法是对实现细节的封装。
  • 继承:从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
  • 多态:指允许不同子类型的对象对同一消息作出不同的响应,简单来说就是用同一对象引用调用同一方法却做了不同的事情。

备注:通常情况下,抽象、封装、继承三大基本特性面试的时候不会问太多,但提到多态,面试官必会深挖。


2. 谈谈对Java中多态的理解?

多态是指允许不同子类型的对象对同一消息作出不同的响应。

多态包括编译时多态和运行时多态。

方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。

运行时多态是面向对象最精髓的东西,要实现运行时多态需要:

  • 方法重写:子类继承父类并重写父类中已有的或抽象的方法。
  • 对象造型:将父类型的引用指向子类型的对象,这样父类的引用调用同样的方法时即会根据子类对象的不同而表现出不同的行为。

3. 在项目中哪些地方使用过多态?

抛砖引玉:实验室预约软件包含学生、教师和管理员三种角色,三者都有login方法,但三者登录后进入的页面却是不同的,即在登录时会有不同的操作。三种角色都继承父类的login方法,但对不同的对象却有不同的行为。


4. Java有哪些访问修饰符?

Java中的访问控制主要分四种级别,如下表:

修饰符 当前类 同包 子类 其他包
public
protected ×
default × ×
private × × ×

注意:Java中外部类的修饰只能是public或默认,类的成员(包括内部类)的修饰可以是以上四种。


5. 在项目中的哪些地方使用过protected修饰符?

相信这个问题,会把部分人搞蒙掉。说实话,我在项目中也没怎么用到过,在此仅做抛砖引玉用,大家有更好的回答欢迎留言哈。

参考答案:

受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。我在项目中确实没怎么使用过,但我知道有一种场景比较适合使用protected修饰符。

Object类中对clone方法的声明即用到了protected访问修饰符,这是因为Object类的clone方法只能实现浅克隆,而并不能实现常使用的深克隆,这就要求子类在需要克隆对象时尽量重写clone方法,此时即声明为protected的,以保证在需要克隆对象时,必须要求待克隆对象所在的类实现Cloneable接口并重写clone方法。

该场景比较抽象,建议好好阅读下「深拷贝与浅拷贝」部分。


6. Java有哪几种基本数据类型?

Java中的基本数据类型只有8个:byte(1字节)、short(2字节)、int(4字节)、long(8字节)、float(4字节)、double(8字节)、char(1字节)、boolean。

除了以上8种基本数据类型,其余的都是引用数据类型。

对应的包装类分别是:Byte、Short、Integer、Long、Float、Double、Character、Boolean。


7. 基本数据类型之间是怎么转换的?

自动类型转换: 容量小的类型自动转换为容量大的数据类型,如下图:

  • 多种类型的数据混合运算时,系统先自动将所有数据转换成容量最大的那种数据类型,再进行计算。
  • 当把任何基本类型的值和字符串值进行连接运算时(+),基本类型的值将自动转化为字符串类型。

强制类型转换: 自动类型转换的逆过程,将容量大的转换为容量小的数据类型。

  • 使用时要加上强制转换符“()”,但可能造成精度降低或溢出。
  • 字符串不能直接转换为基本类型,需要借助基本类型对应的包装类来实现。

扩展问题1:float f=3.4;是否正确?

参考答案:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;

扩展问题2: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);其中有隐含的强制类型转换。


8. Integer类型值的缓存机制是怎样的?

此处仅关于自动装箱与拆箱的经典笔试题,好好思考一下吧。

class AutoUnboxingTest {

    public static void main(String[] args) {
        Integer a = new Integer(3); // 手动创建对象,不会引用常量池中的对象
        Integer b = 3; // 将3自动装箱成Integer类型
        int c = 3;
        System.out.println(a == b); // false 两个引用没有引用同一对象
        System.out.println(a == c); // true a自动拆箱成int类型再和c比较(易错)

        Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;

        System.out.println(f1 == f2); // true
        System.out.println(f3 == f4); // false
        // 易错:比较的是对象的引用;
        // 自动装箱时-128到127引用常量池中的对象,否则新建对象

    }
}

9. Java中重写与重载的区别是什么?

重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载。

重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。

如下图所示:

重写与重载示意图

注:重载是发生在同一个类中,具有相同方法名且有不同参数列表的多个方法间构成重载。而重写是发生在具有继承关系的子类与父类之间,子类可重写父类的方法。

其中,不能根据根据返回类型来区分重载,因为在调用方法时并不会判断方法的返回值类型是什么,如果根据返回值类型来区分重载,则程序会不知道去调用哪个方法。


10. 谈谈对static关键字的理解?

关键字static表示「静态的」,主要思想是保证无论该类是否产生对象或无论产生多少对象的情况下,某些特定的数据在内存空间中只有一份。

在Java类中,可用static修饰属性、方法、代码块和内部类,而不能修饰构造器。

其中,被修饰后的成员具有如下特点:

  • 随着类的加载而加载,故优先于对象存在;
  • 所修饰的成员,被该类的所有对象所共享;
  • 访问权限允许时,可不创建对象,直接被类调用。

关键字static大体上有一下五种用法:

  • 静态导入
  • 静态变量
  • 静态方法
  • 静态代码块
  • 静态内部类

静态导入:

静态导入极少使用,在梳理本篇文章之前真的不知道还有此奇技淫巧,仅作了解即可,示例代码如下:

// 静态导包,在类中使用Math的静态方法和属性时可以省略「Math.」
import static java.lang.Math.*; 
public class StaticImport { 
    public static void main(String[] args) { 
        double a = cos(PI / 2); //已省略「Math.」 
        double b = pow(2.4,1.2); 
        double r = max(a,b); 
        System.out.println(r); 
    } 
}

当在程序中多次使用某类型的静态成员(静态属性和静态方法)时,即可使用静态导入,作用是将该类型的静态成员引入到当前的命名空间,那么在程序中调用该类型的静态成员时可以像调用本类内定义的成员一样,直接调用,而无需采用「类名.静态成员名」的方式。

缺点:虽然简化代码,但可读性大大降低,几乎不使用。

静态变量:

在Java类中,用static修饰的属性为静态变量(类变量或类属性),而非static修饰的属性为实例变量,区别如下:

静态变量VS实例变量

静态方法:

在Java类中,用static修饰的方法为静态方法,也叫类方法,其与非静态方法的对比如下:

静态方法VS非静态方法

注意:静态的结构随着类的加载而加载,其生命周期早于非静态的结构,同时被回收也要晚于非静态的结构。

静态代码块:

静态代码块仅在类加载时运行一次,主要用于对Java类的静态变量(类属性)进行初始化操作。

执行顺序:静态代码块 > 构造代码块(非静态代码块) > 构造方法。

静态内部类:

内部类的一种,静态内部类不依赖于外部类,即可以不依赖于外部类实例对象而被实例化,且不能访问外部类的非静态成员(属性和方法)。


11. Java中变量的初始化顺序是怎样的?

在有继承关系的情况下,变量初始化顺序如下:

  • 父类的静态变量和静态代码块
  • 子类的静态变量和静态代码块
  • 父类的实例变量和普通代码块
  • 父类的构造函数
  • 子类的实例变量和普通代码块
  • 子类的构造函数

扩展问题:指出下面程序的运行结果

class A {
    static {
        System.out.print("1");
    }
    public A() {
        System.out.print("2");
    }
}

class B extends A{
    static {
        System.out.print("a");
    }
    public B() {
        System.out.print("b");
    }
}

public class Hello {
    public static void main(String[] args) {
        A ab = new B();
        ab = new B();
    }
}

执行结果为:1a2b2b。创建对象时构造器的调用顺序是:先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。


12. 谈谈对final关键字的理解?

关键字final表示「最终的」,可用来修饰类、属性和方法.

  • 修饰类:表示该类不能被继承,以提高程序的安全性和可读性,如String、System、StringBuffer类等。
  • 修饰方法:表示方法不能被重写,如Object类的getClass方法。
  • 修饰属性:表示变量一次赋值以后值不能被修改(常量),其名称通常大写。

补充:关键字final修饰的基本类型变量和引用类型变量有区别吗?

对于基本类型变量,final使变量数值在一次赋值后不变;对于引用类型变量,final使变量引用在一次赋值后不变,即不能再引用其他对象,但被引用对象本身的属性是可以修改的。

示例代码如下:

final int COUNT = 1;
// COUNT = 2; // cannot assign value to final variable 'COUNT'

final Employee emp = new Employee();
emp.name = "丙子先生"; // 所引用对象的属性可变

13. 如何初始化final所修饰的成员变量?

关键字final修饰的成员变量没有默认初始化值,其初始化方式主要有:

  • 在声明时,直接对其进行显式初始化。
  • 声明完后,在代码块中对其显式初始化。
  • 声明完后,在构造器中对其显式初始化,但注意需要在所有构造器中均对其进行初始化操作。

基本原则:保证在对象创建之前即需要对final修饰的属性进行初始化操作。


14. 接口与抽象类有什么区别?

接口是对行为的抽象,其可以含有属性和方法。

  • 属性被隐式指定为public static final的,即全局常量。
  • 方法被隐式执行为public abstract的,即抽象方法。

也就是说,接口中的所有方法都必须为抽象方法,不能有具体实现。所以说,接口是对行为的抽象。

抽象类是对类(一类事物)的抽象,《Java编程思想》一书中将抽象类定义为包含抽象方法的类,但准确来说,包含抽象方法的类一定是抽象类,但抽象类不一定有抽象方法,只要用abstract修饰即可为抽象类,但个人认为没有抽象方法的抽象类并没有实际意义吧?

抽象类可以含有属性、方法和构造器。

  • 方法可以是普通方法也可以是抽象方法,若为抽象方法则必须为public或protected的,缺省情况下默认为public的,因为抽象方法需要被子类继承和实现。
  • 构造器虽然有,但因为抽象类含有无具体实现的方法,所以抽象类不能进行实例化。

语法层面上的区别:

  • 成员变量:抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是常量,即public static final修饰的。
  • 成员方法:抽象类中的成员方法可以是抽象的也可以是普通的(有具体实现的),而接口中的成员方法只能是public static修饰的。
  • 静态结构:抽象类中可以有静态代码块和静态方法,而接口中不能有静态代码块和静态方法。
  • 构造方法:抽象类中可以有构造器,而接口中没有,但两者都不能进行实例化,但可以定义抽象类和接口类型的引用。
  • 继承与实现:一个类只能继承一个抽象类,而一个类却可以实现多个接口。

设计层面上的区别:

  • 抽象类是对类(一类事物)的抽象,而接口是对行为的抽象。再具体一点说,抽象类是对一类事物整体(包括属性和行为)进行抽象,而接口是对类的局部(仅对行为)进行抽象。如飞机、鸟和飞行而言,应分别将其设计为类、类和接口。
  • 抽象类作为很多子类的父类,是一种模板式设计,而接口作为一种行为规范,是一种辐射式设计。如果需要添加新的方法,抽象类作为模板,可以直接添加带具体实现的方法而无需改变子类,而接口作为规范,规范改变(添加行为),遵守规范的子类都必须进行相应的改动。

15. 接口与抽象类分别在什么场景下使用?

问题同「你在项目中哪些地方使用过接口和抽象类?具体是怎么使用的?」

建议阅读「门与警报」的例子。

门都有打开和关闭两个行为,此时若需要门具备警报行为,应该如何实现呢?

其实,门的打开和关闭属于门本身固有的行为,而警报功能属于门非固有的行为(附加行为)。最佳解决方案是,将门设计为一个抽象类,包括打开和关闭两种行为,而将警报设计为一个接口,包括警报行为,进而设计一个警报门继承抽象类并实现警报接口即可。


16. 谈谈对内部类的理解?

在Java中,可以将一个类定义在另一个类里面或一个方法内,这样的类即被称为内部类,其框架体系如下:

下面分别对成员内部类、局部内部类和匿名内部类的具体情况进行详解分析。

成员内部类 是最普通的内部类,其定义位于另一个类的内部且在方法外,作为类及类的成员而存在:

  • 作为类,可声明为abstract的,即可以被其他的内部类所继承。
  • 作为类的成员,其可声明为final、static(静态内部类)和abstract的,且与外部类不同的是,内部类可以使用四种访问修饰符进行修饰。

成员内部类访问外部类:成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员),使用「this.成员名」调用内部类的成员,使用「外部类名.this.成员名」调用外部类的成员。但需注意,静态内部类只能访问外部的静态成员。

外部类访问成员内部类:必须先在外部类中创建一个成员内部类的对象,再通过指向该对象的引用来访问内部类的成员。但对于静态内部类和非静态内部类而言,实例化内部类的方式不同:

// 设类ClassA有静态内部类ClassB和普通内部类ClassC
ClassA classA = new ClassA();
ClassA.B classB = new ClassA.B();
ClassA.C classC = classA.new ClassC();

局部内部类 是定义在一个方法内或一个作用域(如if条件判断代码块)中的类,其和成员内部类的区别在于局部内部类的访问仅限于该方法内或该作用域中。

注意事项:

  • 局部内部类可以访问当前代码块的常量以及其外部类的所有成员。
  • 局部内部类非外部类的成员,故外部类无法访问该内部类。
  • 局部内部类可以看做一个局部变量,不能有public、protected、private和static修饰。

匿名内部类 指没有名字的内部类,故其只能使用一次,通常用来简化代码编写,如Android中为控件添加监听事件。

注意事项:

  • 匿名内部类必须继承一个父类或实现一个接口,进而对继承方法进行实现或重写。
  • 匿名内部类是唯一一种没有构造器的类,其在编译时由系统自动起名为「外部类名$序号.class」,如「Outter$1.class」。
  • 匿名内部类一定是在new的后面,仅用于创建该匿名内部类的一个实例。

应用场景:最常用的情况就是在多线程的实现上,因为要实现多线程必须继承Thread类或是继承Runnable接口。

扩展问题:在什么场景中使用过内部类?

这个问题不好回答,在此简单总结下内部类的优势,大家可以据此分析其实际应用场景。

  • 每个内部类都能独立继承一个父类或一个接口的实现,使得多继承的解决方案更加完整。
  • 方便将存在一定逻辑关系的类组织在一起,又可以对外界进行隐藏(体现封装性)。
  • 方便编写事件监听程序和多线程代码。

17. 静态内部类和非静态内部类有什么区别?

在对象创建方面,静态内部类可以不依赖于外部类的实例对象而被实例化,而普通内部类需要在外部类实例化后才能进行实例化。示例代码如下:

// 设类ClassA有静态内部类ClassB和普通内部类ClassC
ClassA classA = new ClassA();
ClassA.B classB = new ClassA.B();
ClassA.C classC = classA.new ClassC();

在是否能够拥有静态成员方面,静态内部类可以拥有静态成员,而普通内部类则不能拥有静态成员。

在访问外部类成员方面,静态内部类只能访问外部类的静态成员,而非静态内部类可以访问外部类的所有成员。

思考:成员内部类、局部内部类和匿名内部类经编译后的字节码文件名称分别是什么?


18. Object类有哪些方法?

Object类位于java.lang包下,是Java中所有类的始祖,其包含的方法列表如下:

各方法的具体说明如下:

Object(): Object类中并没有显式声明该构造方法,其是由编译器自动为其创建的一个不带参数的默认构造器。

registerNatives(): 用于注册本地方法,即将本地用C/C++实现的方法映射到Java中的本地(native)方法,实现方法命名的解耦。

getClass(): 用于获取此Object的运行时类对象,运行时类是 Java 反射机制的源头。

hashCode(): 用于返回对象的哈希值,哈希值是一个可正可负的整数值。

equals(): 用于判断两个对象的引用是否相等,而实际开发中常需要重写该方法以比较两个对象的具体内容是否相等。

clone(): 用于创建并返回此对象的一个副本,其实质上是一种「浅拷贝」。

toString(): 用于返回该对象的字符串表示,建议所有子类均重写该方法,以返回该对象中各属性值的字符串表示。

notify(): 用于唤醒一个在此对象监视器上等待的线程,由JVM决定唤醒哪个等待线程,与线程的优先级无关。

notifyAll(): 用于唤醒所有在此对象监视器上等待的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。

wait(long timeout): 导致当前线程释放所持有对象的锁(线程暂停执行),进入在对象监视器上等待的状态,直到达到最长等待时间(timeout)或其他线程调用notify()或notifyAll()方法。

wait(long timeout, int nanos): 最长等待时间为「1000000 * timeout + nanos」。

wait(): 未指定最长等待时间。

finalize(): 垃圾回收器在准备回收对象前,会先调用该方法;子类可通过重写该方法,以在垃圾回收前整理系统资源或执行其他清理操作。

源码阅读:

package java.lang;

public class Object {

    /**
     * 私有的,在静态代码块中执行,且仅在Object类首次被加载时执行一次。
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    /**
     * final方法:不能被子类所重写
     */
    public final native Class<?> getClass();

    /**
     * 在子类中重写该方法时,可直接调用Objects.hash(Object...)来获取对象的哈希值
     */
    public native int hashCode();

    /**
     * 默认实现中,仅比较两个对象是否引用的同一个对象
     * 实际开发中,需要重写该方法来比较两个对象的具体内容是否相等
     */
    public boolean equals(Object obj) {
        return (this == obj);
    }

    /**
     * 仅对本包下的所有类和当前类的子类可见。
     * 只有实现Cloneable接口的类的对象才能调用该方法,否则会抛出异常
     */
    protected native Object clone() throws CloneNotSupportedException;

    /**
     * 返回:运行时类名@十六进制哈希值字符串
     */
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    /**
     * 唤醒单个线程,但不确定具体唤醒的是哪个线程
     */
    public final native void notify();

    /**
     * 唤醒所有线程,但并不是所有线程都可以拿到对象锁而进入就绪状态
     */
    public final native void notifyAll();

    /**
     * 使当前线程释放对象锁,即线程暂停执行,直到其他线程调用notify()/notifyAll()方法或达到timeout
     */
    public final native void wait(long timeout) throws InterruptedException;

    /**
     * 最大等待时间:1000000*timeout+nanos
     */
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

    /**
     * 未设定最大等待时间,即只能被notify()/notifyAll()唤醒
     */
    public final void wait() throws InterruptedException {
        wait(0);
    }

    /**
     * 子类可通过重写该方法,以在垃圾回收前整理系统资源或执行其他清理操作
     * 在该方法中,若将该对象重新与引用链建立关联关系,则会逃离本次垃圾回收
     */
    protected void finalize() throws Throwable { }
}

19. 重写equals方法的原因、方式和注意事项

为什么要重写equals()方法?

Object类中equals()方法的默认实现主要是用于判断两个对象的引用是否相同。而在实际开发过程中,通常需要比较两个对象的对应属性是否完全相同,故需要重写equals()方法。

如何重写equals()方法?

假设equals()方法的形参名为otherObj,稍后需要将其转换为另一个叫做other的变量。

第一步,检测this与otherObj是否引用同一对象:

if(this == otherObject) return true;

第二步,检测otherObj是否为空:

if(otherObject == null) return false;

第三步,判断this与otherObj是否属于同一个类,具体分两种情况:

(1). 如果equals()方法的语义在每个子类中均有所改变,则使用getClass()方法进行检测:

if(getClass() != otherObject.getClass()) return false;

(2). 如果equals()方法在所有子类中均有统一的语义,则使用instanceof关键字进行检测:

if (!(otherObject instanceof ClassName)) return false;

第四步,将otherObj转换为相应类的类型变量:

ClassName other = (ClassName) otherObject;

第五步,对所有需要比较的域进行一一比较,若全匹配则返回true,否则返回false。

关于equals()语义的补充说明:假设现有Employee与Manager两个类,Manager类继承Employee类。若仅将ID作为相等的检测标准,则仅用在Employee类中重写equals()方法,并将该方法声明为final的即可,这就是所谓的「拥有统一的语义」。

重写equals()方法需要注意什么?

归根结底,还是想问equals()方法的主要特性。Java语言规范要求equals()方法具有如下特性:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true。
  • 对称性:对于任何引用x和y,当且仅当y.equals(x) 返回true时,x.equals(y)也应该返回true。
  • 传递性:对于任何引用x、y和z,如果x.equals(y) 返回true,y.equals(z)返回true,x.equals(z)也应该返回true。
  • 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
  • 非空性:对于任何非空引用x,x.equals(null)应该返回false。

源码阅读:

// Object类中equals()方法的默认实现
public boolean equals(Object obj) {
    return (this == obj);
}
// String类中equals()方法的具体实现
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;
}

20. 重写hashCode方法的原因、方式和注意事项

为什么要重写hashCode()方法?

Object类中hashCode()方法默认是将对象的存储地址进行映射,并返回一个整形值作为哈希码。

若重写equals()方法,使其比较两个对象的内容,并保留hashCode()方法的默认实现,那么两个明明「相等」的对象,哈希值却可能不同。

如果两个对象通过equals()方法比较的结果为true,那么要保证这两个对象的哈希值相等。

因此,在重写equals()方法时,建议一定要重写hashCode()方法。

如何重写hashCode()方法?

由于Object类的 hashCode() 方法是本地的(native),故其具体实现并不是由Java所完成的。

需要实现hashCode()方法时,可以直接调用Objects.hash(Object... values)方法来获取对应的哈希值。其内部的具体实现是调用Arrays.hashCode(Object[])方法来完成的。

Arrays中计算哈希值的核心代码如下:

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

那么,为什么选择数字31作为计算哈希值的因数?

The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance:31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically. ------引用自《Effective Java》

重写hashCode()方法需要注意什么?

  • 应用程序执行期间,只要一个对象用于equals()方法的属性未被修改,则该对象多次返回的哈希值应相等。
  • 如果两个对象通过equals()方法比较的结果为true,那么要保证这两个对象的哈希值相等。
  • 如果两个对象通过equals()方法比较的结果为false,那么这两个对象的哈希值可以相等也可以不相等,但理想情况下是应该不相等,以提高散列表的性能。

源码阅读:

// Object类中hashCode()方法的默认实现
public native int hashCode();
// Arrays中计算哈希值的核心代码
public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

21. Class类是什么?

先来简单了解一下Java虚拟机中类的加载过程:

「加载」阶段是「类加载」过程的第一个阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(.class文件即保存着类的二进制数据)。
  2. 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表该类的java.lang.Class对象,作为方法区中该数据结构的访问入口。

也就是说,Class是一个保存着运行时类所有信息的类,即在程序运行时跟踪类且掌握着类的全部信息,故其也被称为反射的源头(有点儿小抽象)。


22. 如何获取Class类的实例?

共有四种方式来获取Class类的实例,如下:

@Test
// 需要提供异常处理
public void test() throws ClassNotFoundException{
    //1.通过运行时类的.class属性获取
    Class<Person> clazz1 = Person.class;
    System.out.println(clazz1);

    //2.通过运行时类的对象获取
    Person p = new Person();
    Class clazz2 = p.getClass();
    System.out.println(clazz2.getName());

    //3.通过Class类的静态方法获取
    String className = "com.whutqbchen.java.Person";
    Class clazz3 = Class.forName(className);
    System.out.println(clazz3);

    //4.通过类的加载器获取
    ClassLoader cl = this.getClass().getClassLoader();
    Class clazz4 = cl.loadClass(className);
    System.out.println(clazz4.getName());
}

提示:在启动时,包含main方法的类被加载。它会加载所有需要的类。这些被加栽的类又要加载它们需要的类,以此类推。对于一个大型的应用程序来说,这将会消耗很多时间,用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的幻觉。不过,要确保包含main方法的类没有显式地引用其他的类。首先,显示一个启动画面;然后,通过调用Class.forName手工地加载其他的类。


23. Class类的实例可以做什么?

获取到运行时类的Class实例后,通过Class类的实例可以:

  • 通过newInstance()方法创建对应运行类的对象。
  • 获取其对应类的完整结构,如构造器、属性、方法、内部类、父类、所在的包、异常和注解等。
  • 调用对应的运行时类的指定结构,如属性、方法和构造器。
  • 反射的应用,即动态***(又挖一坑)。

所以说,Class类是反射的源头,有一种「Class在手天下我有」的既视感。

这也是Java中「反射」技术的主要内容。

注意事项:

请注意,一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。

另外,调用Class类实例的newInstance()方法动态创建类对象时,需要对应的运行时类中有空参的构造器。

通过Class类的实例获取运行时类的所有描述信息的代码较长,在此仅给出对应的方法描述,如下:

通过反射可访问的主要描述信息


24. 深拷贝和浅拷贝的区别和具体实现

浅拷贝和深拷贝有什么区别?

先来了解一下两个概念:「引用拷贝」和「对象拷贝」。

「引用拷贝」是指创建一个指向对象的引用变量的拷贝,例如:

Employee emp1 = new Employee("Taylor", 26);
Employee emp2 = emp1;
System.out.println(emp1); // Employee@355da254
System.out.println(emp2); // Employee@355da254

即emp1和emp2指向堆空间中的同一个对象,这就叫「引用拷贝」。

而「对象拷贝」是指创建对象本身的一个副本,例如:

Employee emp1 = new Employee("Swift", 26);
Employee emp2 = (Employee) emp1.clone();
System.out.println(emp1); // Employee@7852e922
System.out.println(emp2); // Employee@4e25154f

即emp1和emp2分别指向堆空间中的不同对象,这就叫「对象拷贝」,但需要注意的是,使用clone()方法进行对象拷贝时,必须要求Employee类实现Cloneable接口并重写clone()方法,且上述代码段所在的方法还需要处理CloneNotSupportedException异常。

其中,「浅拷贝」和「深拷贝」都属于「对象拷贝」

对于基本数据类型的成员变量,无论「浅拷贝」还是「深拷贝」都会直接进行值传递,原对象和新对象的该属性值为两份数据,相互独立,且互不影响。

而对于引用类型的成员变量,「浅拷贝」仅复制该属性的引用而不复制引用所指向的对象,即原对象和新对象的该属性指向的是同一个对象;「深拷贝」则会直接复制该属性所指向的对象,即原对象和新对象的该属性指向的是堆空间中内存地址不同的对象。

如何实现「浅拷贝」?

要求待拷贝对象所属类:

  • 实现Cloneable接口;
  • 重写clone()方法,并指定public访问修饰符。

要求在调用待拷贝对象的clone()方法时:

  • 处理编译时异常:CloneNotSupportedException。

另外,也可以手动采用赋值的方式将原对象的各个属性值拷贝到新的对象。

如何实现「深拷贝」?

方式1:通过实现Cloneable接口并重写clone()方法来实现,即将原对象及其所引用的所有对象所属的类均实现Cloneable接口并重写clone()方法。

方式2:通过序列化方式来实现。由于篇幅有限,具体实现暂且先埋一个坑吧!

Object类的clone()方法为什么要声明为protected?

若声明为public的,则在子类不重写clone()方法时,调用的还是Object类的clone()方法,只能实现浅拷贝。而声明为protected的,就要求子类在需要拷贝对象时,必须要要实现Cloneable接口并重写clone()方法,在其中既可实现浅拷贝也可实现深拷贝,但通常都需要实现深拷贝。


25. 谈谈对反射和动态***的理解?

动态***作为设计模式中动态***模式的一部分,其和静态***构成鲜明对比。下面分别对***模式、静态***和动态***技术点进行详细介绍与分析。

***模式 的主要思想是创建一个***对象来包装原始对象,任何对原始对象的调用必须通过***对象,并由***对象来决定是否以及何时将方法调用转到原始对象上。

静态*** 的主要特征是***类和被***类都是在编译期间就确定下来的,一个***类仅能为一个接口服务,不利于程序的扩展。

实现步骤:

  • 创建被***类所实现的接口(包括抽象方法);
  • 创建被***类,实现相应的接口并重写其抽象方法;
  • 创建***类,实现被***类所实现的接口,创建构造器并传入被***类的对象,在重写接口的抽象方法中发起对被***类的调用。

示例代码:

//1.创建***类所实现的接口 
interface ClothFactory {
    void productCloth();
}

// 2.创建被***类,并实现相应的接口
class NikeClothFactory implements ClothFactory {
    // 重写抽象方法
    public void productCloth() {
        System.out.println("Nike-Nike!");
    }
}

// 3.创建***类,实现被***类所实现的接口
class ProxyFactory implements ClothFactory {
    ClothFactory cf;
    // 创建构造器,传入的是被***类的对象!
    public ProxyFactory(ClothFactory cf) {
        this.cf = cf;
    }
    // 重写抽象方法,发起对被***对象方法的调用
    public void productCloth() {
        cf.productCloth();
    }
}

public class TestStaticProxy {
    public static void main(String[] args) {
        //1.创建被***类对象
        NikeClothFactory ncf = new NikeClothFactory();
        //2.创建***类对象
        ProxyFactory pf = new ProxyFactory(ncf);
        //3.执行***类的方法,但实际发起的是对被***类中指定方法的调用
        pf.productCloth();
    }
}

在代码上看,执行的是***类的方法,但实际是通过***类方法发起对被***类方法的调用。

动态*** 主要是为了解决静态***中一个***类仅能服务一个接口的问题。

动态***的主要特征是通过***类来调用其它对象的方法,并且是在程序运行时根据需要动态创建被***类的***对象。

实现步骤:

  • 创建被***类所实现的接口(包括抽象方法);
  • 创建被***类,实现相应的接口并重写其抽象方法;
  • 创建一个实现InvocationHandler接口的***类,重写其invoke()方法以完成***的具体操作,同时创建blind()方法一方面完成对被***类对象的实例化,另一方面通过Proxy的静态newProxyInstance()方法返回一个动态***类的对象

示例代码:

// 1.被***类所实现的接口
interface Subject {
    void action();
}

// 2.创建被***类,实现接口并重写接口的抽象方法
class RealSubject implements Subject { @Override public void action() {
        System.out.println("被***类-被***");
    }
}

// 3.创建***类,实现InvocationHandler接口
class MyInvocationHandler implements InvocationHandler {
    Object obj;
    // 3.1 重写invoke方法,完成***的具体操作 @Override public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //method为被***类的目标方法
        Object returnVal = method.invoke(obj, args);//调用被***类的action方法
        return returnVal;
    }
    // 3.2 实例化被***对象,返回一个***类对象
    public Object blind(Object obj) {
        this.obj = obj;
        // 返回一个***类对象
        return
        Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

}

public class TestProxy {
    public static void main(String[] args) {
        //1.创建被***类的对象
        RealSubject real = new RealSubject();
        //2.创建***类的对象
        MyInvocationHandler handler = new MyInvocationHandler();
        //3.调用blind()方法,动态返回一个同样实现被***类所实现接口的***类的对象
        Object obj = handler.blind(real);
        Subject sub = (Subject)obj;//此时的sub即为***类的对象
        sub.action();

        NikeClothFactory ncf = new NikeClothFactory();
        obj = handler.blind(ncf);
        ClothFactory cf = (ClothFactory)obj; //此时的cf即为***类的对象
        cf.productCloth();
    }
}

扩展:动态***之Proxy类

Proxy类是专门完成***的操作类,其是所有动态***类的父类,通过此类为一个或多个接口动态地生成实现类。其提供的主要静态方法如下:

// 获取动态***类所对应的Class对象
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)  

// 创建动态***对象
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

26. String、StringBuffer与StringBuilder的区别

问题分析:先来了解一下Java虚拟机对「string +="hello"」的优化,优化结果为:

StringBuilder str = new StringBuilder(string);
str.append("hello");
str.toString();

这是因为StringBuilder类表示可变的字符序列,其效率比String类高很多。

而StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer的成员方法被synchronized修饰,故StringBuffer类也表示可变的字符序列,但其是线程安全的,而StringBuilder类是线程不安全的。

参考答案:

  • 线程安全,只有StringBuffer类是线程安全的,String类和StringBuffer类都是线程不安全的。
  • 执行效率:通常情况下,StringBuilder > StringBuffer > String,但也有特例,具体如下:
String str = "hello"+ "world"; // 效率高,编译器会对其字符串的直接相加操作进行优化
StringBuilder st  = new StringBuilder().append("hello").append("world");

优化策略如下:

  • 直接相加:形如"I"+"love"+"java"的字符串相加操作,在编译期间即会被优化成"Ilovejava",可以用javap -c命令反编译生成的class文件进行验证。
  • 间接相加:即包含字符串引用的相加操作,形如s1+s2+s3,效率要比直接相加低,因为编译器不会对引用变量进行优化。

扩展问题1:String str = new String("java")创建了多少个对象?

答:准确来说,在类加载的过程中,该段代码在运行时常量池中创建了一个"java"对象,而在代码执行过程中在堆空间创建了一个String对象。故该段代码在运行期间仅创建了一个对象,但该段代码涉及到了两个String对象。

扩展问题2:关于String类的笔试题(重点关注)

public static void main(String[] args) {
    String stra = "hello2";
    String strb = "hello" + 2; //在编译期间已被优化成"hello2"
    System.out.println(stra == strb); //true

    String strc = "hello2";
    String strd = "hello"; 
    String stre = strb + 2; //字符串引用不会在编译期间被优化
    System.out.println(strc == stre); //false

    String strf = "hello2";
    final String strg = "hello"; //final修饰的变量编译时会在常量池保存一个副本
    String strh = strg + 2; //对final变量的访问在编译期间都会直接被替代为真实的值
    System.out.println(strf == strh); //true

    String stri = "hello2";
    final String strj = getHello(); //值是运行期间才确定的
    String strk = strj + 2; 
    System.out.println(stri == strk); //false
}

public static String getHello() {
    return "hello";
}

27. Exception与Error有何异同?

问题分析:既然问到两者的区别,说明两者是有必然联系的,故简单从两者的联系说起,重点突出两者的区别,最后可选择性的介绍Exception的体系结构。

参考答案:

联系:Exception和Error都继承于Throwable类,在Java中只有Throwable类的实例才可被抛出或者捕获,故可将Throwable类看做Java中异常与错误处理的核心。

区别:Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。而Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。

常见的Error主要包括:

  • OutOfMemoryError:内存溢出错误
  • StackOverflowError:栈溢出错误
  • VirtualMachineError:虚拟机错误
  • NoClassDefFoundError:找不到类错误

28. 编译时异常和运行时异常的区别

Exception类作为Java中异常处理机制的基本组成类型,其主要分为编译时异常和运行时异常。

编译时异常也叫可检查异常,指编译器要求必须处置的异常,即编译时异常不是说有异常才处理,而是在编译前对可能出现的异常隐患进行提示并要求处理。

运行时异常也叫不检查异常,指编译器不要求强制处置的异常,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

总而言之,对于运行时异常来说,可以不显式地进行处理;而对于编译时异常来说,必须要显式地进行处理。

扩展问题:如下代码块的返回值是什么?

public static int WithException() {
    int i = 10;
    try {
        i = i / 0;
        return --i;
    } catch (Exception e) {
        --i;
        return --i;
    } finally {
        --i;
        return --i;
    }
}

答:6。因为finally代码块的存在,try和catch中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,但由于finally中有return语句,故直接从finally代码块中直接返回。(不太好理解,大家可以debug看一下)


29. 常见的运行时异常有哪些?

  • ArithmeticException(算术异常)
  • ClassCastException (类转换异常)
  • IllegalArgumentException (非法参数异常)
  • IndexOutOfBoundsException (下标越界异常)
  • NullPointerException (空指针异常)
  • SecurityException (安全异常)

30. final、finally与finalize的区别

关键字final表示「最终的」,可用来修饰类、属性和方法.

  • 修饰类:表示该类不能被继承,以提高程序的安全性和可读性,如String、System、StringBuffer类等。
  • 修饰方法:表示方法不能被重写,如Object类的getClass方法。
  • 修饰属性:表示变量一次赋值以后值不能被修改(常量),其名称通常大写。

finally关键字是对Java异常处理模型的最佳补充。无论是否有异常发生,finally块中的代码总会被执行,其常用于执行资源清除操作,如关闭文件读写流、关闭数据库连接等。

finalize是Object类的一个方法,在垃圾回收器执行对象回收操作前会先调用该对象的finalize方法,可覆盖此方法来提供垃圾回收时的其他资源回收,如关闭文件等。


31. 什么是Java的序列化和反序列化?

在Java中,序列化是指将Java对象转换为字节序列的过程,而反序列化是指将字节序列转换为Java对象的过程。其中,字节序列即是二进制数据,可以方便地在网络上传输或存储到本地硬盘中。

序列化的主要作用包括两个方面:

  • 实现网络中对象的传送,即在网络进程间传递对象。
  • 实现内存中对象的持久化,即将对象保存到本地磁盘中。

主要应用场景:

  • Java远程方法调用(RMI),即允许一个Java虚拟机上运行的Java程序调用其他虚拟机运行内存中对象的方法,即使这些虚拟机运行于物理隔离的不同主机上。
  • 分布式系统中不同服务器间共享的JavaBean对象都需要先序列化为二进制数据,再在网络中传输。
  • Session钝化机制:Web容器将一些session先序列化写入本地磁盘,在需要使用时再将对象从磁盘还原到内存中去,以减轻服务器的内存负担。

备注:当然还有很多应用场景,在此仅做参考。


32. 如何实现Java中对象的序列化?

问题分析:同「请解释一下Serializable接口的作用」。

参考答案:

第一步:将需要序列化的对象所属类实现java.io.Serializable标记接口(没有任何抽象方法的接口)。

public class Person implements Serializable {
    private static final long serialVersionUID = -7755892886656448346L;
    private String name;
    private Integer age;
    // getter、setter、constructor和toString方法省略
}

第二步:在Java程序中使用对象输出流(ObjectOutputStream),通过其writeObject()方法来实现序列化操作。

OutputStream out = new FileOutputStream("D:\\Jerry.txt");
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(new Person("Jerry", 18));
oos.flush();

第三步:在Java程序中使用对象输入出流(ObjectInputStream),通过其readObject()方法来实现反序列化操作。

InputStream is = new FileInputStream("D:\\Jerry.txt");
ObjectInputStream ois = new ObjectInputStream(is);
Person jerry = (Person) ois.readObject();

常见问题:

  • 若对象所属类未实现Serializable接口,则在序列化时会报java.io.NotSerializableException运行时异常。
  • 建议对象所属类在实现Serializable接口的同时,添加serialVersionUID常量。

注意事项:

  • 序列化时,仅对对象的状态(属性值)进行保存,而不管对象的方法。
  • 静态成员数据不能被序列化,因为static代表类的状态。
  • 在变量声明前添加transient关键字,可在序列化时忽略该数据。

扩展问题1:如何实现自定义序列化策略?

方式一:实现Externalizable接口,并重写WriteExternal(ObjectOutput)和readExternal(ObjectInput)方法。其中,Externalizable接口继承了Serializable接口,并添加了以上两个方法,用于实现对序列化的控制。

方式二:根据Externalizable接口的思想,可在实现Serializable接口的基础上,直接添加并实现WriteObject(ObjectOutputStream)和readObject(ObjectInputStream)两个方法。

其中,ArrayList类即采用方式二来实现对序列化的控制,主要是为了保证仅对动态数组中的非null元素进行序列化。

扩展问题2:serialVersionUID常量有什么作用?

答:保证版本号的一致性。若不显式指定该常量值,Java编译器会自动为其生成一个默认值,该默认值会随着类代码的变动而变动。故当修改类代码时,之前已经序列化的对象在进行反序列化时即会因为版本号的不一致而发生异常。

显式定义serialVersionUID主要有两种用途:

  • 在某些场合,希望类的不同版本对序列化兼容,故需确保类的不同版本具有相同的serialVersionUID;
  • 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

扩展问题3:序列化为什么可以实现深拷贝?

答:若一个对象的属性引用其他对象,则序列化该对象时引用对象也会同时被序列化,这即是序列化能够实现深拷贝的本质。


33. Java中四种引用的基本概念、具体实现、声明周期和应用场景

Java将引用分为强引用、软引用、弱引用和虚引用四种,这四种引用强度依次逐渐减弱。

下面分别从四种引用的基本概念、具体实现、生命周期和应用场景四个方面进行对比分析。

强引用(Strong Reference):

  • 基本概念:在程序代码中普遍存在的,类似于“Object obj = new Object()”形式的引用。
  • 具体实现:创建一个对象并将其赋值给一个引用变量。
  • 生命周期:垃圾收集器永远不会回收掉强引用所关联着的对象,除非将该对象的所有引用全部置为null。
  • 应用场景:用的太多,以至于我说不出来(撒手状)。

软引用(Soft Reference):

  • 基本概念:描述一些还有用,但并非必需的对象。
  • 具体实现:JDK提供了SoftReference类来实现。
  • 生命周期:在系统将要发生内存溢出异常之前,将会把软引用所关联着的对象列进回收范围内并进行第二次回收,若回收后仍没有足够内存则抛出内存溢出异常。
  • 应用场景:用于实现内存敏感的高速缓存,如网页缓存和图片缓存等。

注意:使用软引用能防止内存泄漏,增强程序的健壮性。

弱引用(Weak Reference):

  • 基本概念:也描述非必需对象,强度比软引用更弱。
  • 具体实现:JDK提供了WeakReference类来实现。
  • 生命周期:无论系统内存是否足够,垃圾收集器工作时都会受到只被弱引用关联着的对象。
  • 应用场景:多将对象的弱引用作为HashMap的key值,以实现在不需要该对象时,只需要在程序中将其强引用置为null,而无需手动将其从HashMap中移除(由垃圾收集器实现,WeakHashMap即是基于该原理)。

虚引用(Phantom Reference):

  • 基本概念:也称幽灵引用或幻影引用,是最弱的一种引用关系。
  • 具体实现:JDK提供了PhantomReference类来实现。
  • 生命周期:虚引用并不会影响其所关联对象的生存时间,也无法通过虚引用来取得一个对象实例。
  • 应用场景:虚引用可以在对象被收集器回收时收到一个系统通知,即多用于在对象销毁前执行一些操作,如资源释放等。

扩展代码阅读:

软引用的具体使用:

// 建立软引用并丢弃强引用
MyObject obj = new MyObject();
SoftReference softRef = new SoftReference(obj);
obj = null; //丢弃强引用

// 通过软应用获取强引用
// 若软引用所关联的对象未被回收则重新获得强引用
// 若已被回收,则anotherObj = null
MyObject anotherObj = (MyObject)softRef.get();

/**
 * 若get()返回null,则说明软引用所关联的对象obj已被回收
 * softRef对象不再具有存在的价值
 * 可使用ReferenceQueue来将其清除,以避免发生内存泄漏
 */
MyObject obj = new MyObject();
ReferenceQueue queue = new ReferenceQueue();
SoftReference ref = new SoftReference(obj, queue);
obj = null;

// 若ref所关联的对象被回收,则ref会自动添加到queue队列中去。

弱引用的具体使用:与软引用雷同,其可以和一个引用对列(ReferenceQueue)联合使用,如果弱引用所关联的对象被JVM回收,该弱引用就会被添加到与之关联的引用对列中。

虚引用的具体使用:必须和引用对列关联使用,具体流程如下:

  • 对于虚引用所关联的对象,垃圾收集器工作时会将该虚引用添加到与之关联的引用对列中;
  • 程序通过判断引用对列中是否已经加入了虚引用,来了解被引用对象是否将要被回收;
  • 若将要被回收,则可以在其被回收之前采取必要的操作。

虚引用的测试代码如下:

Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, queue);
obj=null;

while(true){
    System.out.printf("pf.get() = %d, isEnqueued: %b\r\n", pf.get(), pf.isEnqueued());
    if(pf.isEnqueued())
        break;
    System.gc();
    Thread.sleep(1000);                
}

// pf.get() = null, isEnqueued: false
// pf.get() = null, isEnqueued: true

扩展问题:Java扩展为四种引用的目的是什么?

  • 有利于Java虚拟机进行垃圾回收;
  • 可以让程序员通过代码的方式来决定某些对象的生命周期。

34. 什么是同步IO和异步IO、阻塞IO和非阻塞IO?

同步IO和异步IO:

IO操作主要分为两个步骤,即发起IO请求和实际IO操作,同步IO与异步IO的区别就在于第二个步骤是否阻塞。

若实际IO操作阻塞请求进程,即请求进程需要等待或者轮询查看IO操作是否就绪,则为同步IO。

若实际IO操作并不阻塞请求进程,而是由操作系统来进行实际IO操作并将结果返回,则为异步IO。

阻塞IO和非阻塞IO:

IO操作主要分为两个步骤,即发起IO请求和实际IO操作,阻塞IO与非阻塞IO的区别就在于第一个步骤是否阻塞。

若发起IO请求后请求线程一直等待实际IO操作完成,则为阻塞IO。

若发起IO请求后请求线程返回而不会一直等待,即为非阻塞IO。


35. BIO、NIO与AIO的概念与区别

BIO表示同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

NIO表示同步非阻塞IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

AIO表示异步非阻塞IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成IO操作后再通知服务器应用来启动线程进行处理。

应用场景:

  • BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,JDK 1.4以前的唯一选择。
  • NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,编程复杂,JDK 1.4开始支持,如在Netty框架中使用。
  • AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持。

备注:在大多数场景下,不建议直接使用JDK的NIO类库(门槛很高),除非精通NIO编程或者有特殊的需求。在绝大多数的业务场景中,可以使用NIO框架Netty来进行NIO编程,其既可以作为客户端也可以作为服务端,且支持UDP和异步文件传输,功能非常强大。


36. Lambda表达式的作用和基本语法

Lambda表达式是Java8所引入的新特性之一,其基于函数式接口,以极大地减少代码冗余,并提高代码的可读性。

Java8中引入箭头操作符(也叫Lambda操作符)将Lambda表达式拆分成左右两部分:

  • 左侧:指定Lambda表达式的参数列表;
  • 右侧:指定Lambda表达式所要执行的功能,即Lambda体。
// 1).无参数,无返回值
Runnable r1 = () -> System.out.println("Hello Lambda!");

// 2).有一个参数,无返回值(只有一个参数时,小括号可以省略)
Consumer<String> con = (x) -> System.out.println(x);
con.accept("Hello Lambda!);

// 3).有两个及以上参数,有返回值,且Lambda体中有多条语句
Comparator<Integer> com = (x, y) -> {
    System.out.println("函数式接口");
    return Integer.compare(x, y);
};

// 若lambda体中仅有一条语句,大括号和return都可以省略

什么是函数式接口?

Lambda表达式需要函数式接口的支持。

仅包含一个抽象方法的接口,称为「函数式接口」。可以在接口上使用@FunctionalInterface注解来检查该接口是否为函数式接口。

注意:函数式接口是Lambda表达式可以使用的关键所在,若一个接口中包含多个抽象方法,是没有办法仅通过表达式左侧的参数列表来定位到对应的方法。

为什么要使用Lambda表达式?

仅对比下面的两个代码实现:

// 实现一
Comparator<String> com = new Comparator<String>() { @Override public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
};
TreeSet<String> ts = new TreeSet<>(com);

// 实现二:基于Lambda表达式
Comparator<String> com = (x, y) -> Integer.compare(x.length(), y.length());
TreeSet<String> ts = new TreeSet<>(com);

这两个代码都是实现的同一个功能,即创建带比较器的TreeSet集合,基于Lambda表达式的实现代码量少,可读性强。

备注:篇幅有限,此处仅给出一个对比,有兴趣可以再深入学习。


特意把公众号文章整理了一下,回馈给各位牛友,为遵守规范特意删除了和公号相关的敏感信息,若感兴趣可以私聊。

更多模拟面试

7条回帖

回帖
加载中...
话题 回帖

资源分享近期热帖

近期精华帖

热门推荐