Java语言常见面试题


1.说一说你对static关键字的理解

在Java类里只能包含成员变量、方法、内部类3种成员,都可以使用static修饰,外加代码块可以使用static修饰,以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。

类成员储存在方法区中,对象是存放在堆内存的。

对static关键字而言,有一条非常重要的规则:类成员不能访问实例成员,实例成员可以访问类成员。因为JVM的初始化顺序是先静态域,然后是非静态域,所以可能出现类成员已经初始化完成,但实例成员还没有初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

16.什么是代码块

代码块又叫初始化块,属于类中的成员,即类的一部分 类似于方法,将逻辑语句封装在方法提中,通过{}定义的一段代码。但和方法不同,没有方法名,没有返回值,没有参数,只有方法体,而且不用通过对象或类显示调用,而是在加载类的时候或者创建对象的时候隐式调用。

  1. 静态代码块:使用static关键字去定义,静态代码块中的代码会在JVM类加载时运行,且只被执行一次。

  2. 构造代码块:和静态代码块的唯一区别就是没有static修饰

  3. 普通代码块:定义在方法中的代码块,一般不会这么无聊去使用它。

    public class Test {
        public static void main(String[] args) {
            //直接使用{}定义普通代码块
            {
                int x=10;
                System.out.println("x1="+x);
            }
            
            int x=100;
            System.out.println("x2="+x);
        }
    }

    无论这三部分的位置如何放置,都是静态代码块先执行,然后是构造代码块。静态代码块只会执行一次,构造代码块在每次实例化对象时都会执行。

2.什么是静态内部类

如果使用static来修饰一个内部类,则这个内部类就被称为静态内部类。

静态内部类需满足如下规则:

  1. 静态内部类可以包含静态成员,也可以包含非静态成员;
  2. 静态内部类不能访问外部类的实例成员,只能访问它的静态成员;
  3. 外部类的所有方法、初始化块都能访问其内部定义的静态内部类;
  4. 在外部类的外部,也可以实例化静态内部类,语法如下:外部类.内部类 变量名 = new 外部类.内部类构造方法();

3.说一说你对final关键字的理解

  • 修饰对象:成员变量,方法,类,局部变量(方法中的变量)
  • final修饰的属性的初始化可以在编译期,也可以在运行期,初始化后不能被改变。
  • final修饰的方法表示该方法在子类中不能被重写,final修饰的类表示该类不能被继承。
  • 对于基本类型数据,final会将值变为一个常数(创建后不能被修改);但是对于对象句柄(亦可称作引用或者指针),final会将句柄变为一个常数(进行声明时,必须将句柄初始化到一个具体的对象。而且不能再将句柄指向另一个对象。但是,对象的本身是可以修改的。这一限制也适用于数组,数组也属于对象,数组本身也是可以修改的。方法参数中的final句柄,意味着在该方法内部,我们不能改变参数句柄指向的实际东西,也就是说在方法内部不能给形参句柄再另外赋值)。

3.static final和final static有什么区别吗?

没区别,写法问题

4.static final是什么?

static final修饰的属性表示一旦给值,就不可修改,并且可以通过类名访问。

static final也可以修饰方法,表示该方法不能重写,可以通过类名调用。

5.final、finally、finalize的区别?

  1. final就是不可变的意思,可以修饰成员变量、局部变量、⽅法和类。修饰变量时,这个变量必须初始化,所以也称为常量。
  2. finally是异常处理的⼀部分,只能⽤在try/catch中,并且附带⼀个语句块表示这段语句⼀定会被执⾏,⽆论是否抛出异常。
  3. finalize是java.lang.Object中的⽅法,也就是每⼀个对象都有这个⽅法,⼀个对象的finalize⽅法只会调⽤⼀次,调⽤了不⼀定被回收,因为只有对象被回收的时候才会被回收,就会导致前⾯调⽤,后⾯回收的时候出现问题,不推荐使⽤。

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

  • 浅拷⻉:浅拷⻉创建⼀个新对象,然后将原对象的⾮静态字段复制到新对象。如果字段是基本数据类型,那么就复制其值;如果字段是引⽤类型,复制的就是引⽤⽽不是实际对象。
  • 创建⼀个新对象,并递归复制原对象中的所有引⽤类型的字段指向的对象,⽽不是共享引⽤。因此,新对象和原对象中的引⽤类型字段引⽤的是两组不同的对象。

总结:

  • 浅拷⻉复制对象,包括对象的引⽤,两者共享引⽤对象。

  • 深拷⻉复制对象及其引⽤的对象,两者不共享引⽤对象,即使引⽤对象也会被复制。

  • 在Java中,浅拷⻉通常通过 clone ⽅法实现,深拷⻉可以通过⼿动实现 clone ⽅法、序列化和反序列化,或

    者使⽤第三⽅库来实现。

7.反射机制是什么?

反射的核心类是Class类,它是Java反射机制的入口。通过Class类,可以获取类的构造器、方法、字段等信息,而不需要在编译时确定这些信息。

反射的基本步骤包括获取Class对象,获取类的信息(构造器、方法、字段等),然后通过Constructor、Method、Field等类的实例进行具体的操作。

优点:程序可以在运行时动态的获取和操作类的信息,这使得代码更加灵活,能够使用不同的场景和需求。

缺点:反射允许绕过编译时的类型检查,这可能导致在运⾏时出现类型安全性问题,并且它涉及到在运⾏时进⾏类型检查和动态分派。这可能导致性能开销。

8.如何对Integer和Double类型判断值相等?

Integer、Double不能直接进行比较,这包括:

  • 不能用==进行直接比较,因为它们是不同的数据类型;
  • 不能转为字符串进行比较,因为转为字符串后,浮点值带小数点,整数值不带,这样它们永远都不相等;
  • 不能使用compareTo方法进行比较,虽然它们都有compareTo方法,但该方法只能对相同类型进行比较。

正确方法:整数、浮点类型的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以,可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较。

Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());

9.==和equals()有什么区别?

==运算符:

  • 作用于基本数据类型时,是比较两个数值是否相等;
  • 作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;

equals()方法:

  • 没有重写时,Object默认以 == 来实现,即比较两个对象的内存地址是否相同;
  • 进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。

10.说一说hashCode()和equals()的关系

hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:

  • 如果两个对象相等,则它们必须有相同的哈希码。
  • 如果两个对象有相同的哈希码,则它们未必相等。

扩展阅读:

在Java中,Set接口代表无序的、元素不可重复的集合,HashSet则是Set接口的典型实现。

当向HashSet中加入一个元素时,它需要判断集合中是否已经包含了这个元素,从而避免重复存储。由于这个判断十分的频繁,所以要讲求效率,绝不能采用遍历集合逐个元素进行比较的方式。实际上,HashSet是通过获取对象的哈希码,以及调用对象的equals()方法来解决这个判断问题的。

HashSet首先会调用对象的hashCode()方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则HashSet会调用equals()对两个对象进行比较。若相等则说明对象重复,此时不会保存新加的对象。若不等说明对象不重复,但是它们存储的位置发生了碰撞,此时HashSet会采用链式结构在同一位置保存多个对象,即将新加对象链接到原来对象的之后。之后,再有新添加对象也映射到这个位置时,就需要与这个位置中所有的对象进行equals()比较,若均不相等则将其链到最后一个对象之后。

11.为什么要重写hashCode()和equals()?

Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。

⽽由于hashCode()与equals()具有联动关系(如果两个对象相等,则它们必须有相同的哈希码),所以equals()⽅法重写时,通常也要将hashCode()进⾏重写,使得这两个⽅法始终保持⼀致性。


字符串

12.String类可以被继承吗?

String类由final修饰,所以不能被继承。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    //省略
}

扩展阅读

在Java中,String类被设计为不可变类,主要表现在它保存字符串的成员变量是final的。

  • Java 9之前字符串采用char[]数组来保存字符,即 private final char[] value;
  • Java 9做了改进,采用byte[]数组来保存字符,即 private final byte[] value;

之所以要把String类设计为不可变类,主要是出于安全和性能的考虑,可归纳为如下2点。

  1. 是保证了线程安全,由于 String 的不可变,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。如果在多线程的环境中,我们需要对某个字符串频繁的修改,可以使用StringBuffer类。
  2. 节约内存,JVM里面有字符串常量池,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。

13.使用字符串时,new和””更推荐使用哪种方式?

先看看 “hello” 和 new String(“hello”) 的区别:

  • 当Java程序直接使用 “hello” 的字符串直接量时,JVM将会使用常量池来管理这个字符串;
  • 当使用 new String(“hello”) 时,JVM会先使用常量池来管理 “hello” 直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。

显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。

14.字符串拼接

拼接字符串有很多种方式,其中最常用的有4种,下面列举了这4种方式各自适合的场景。

  1. + 运算符:如果拼接的都是字符串字面量,则适合使用 + 运算符实现拼接;
  2. StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;
  3. StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;
  4. String类的concat方法:如果只是对两个字符串进行拼接,并且包含变量,则适合使用concat方法;

扩展阅读

采用 + 运算符拼接字符串时:

  • 如果拼接的都是字符串字面量,拼接操作在编译器的编译期间就执行了,会直接把拼接后的常量放入字符串常量池中。
  • 如果拼接操作包含字符串引用,那么它是在Java运行期间执行的,新创建的字符串存放在堆内存中。

采用StringBuilder/StringBuffer拼接字符串时:

  • StringBuilder/StringBuffer都有字符串缓冲区,缓冲区的容量在创建对象时确定,并且默认为16。当拼接的字符串超过缓冲区的容量时,会触发缓冲区的扩容机制,即缓冲区加倍。
  • 缓冲区频繁的扩容会降低拼接的性能,所以如果能提前预估最终字符串的长度,则建议在创建可变字符串对象时,放弃使用默认的容量,可以指定缓冲区的容量为预估的字符串的长度。

采用String类的concat方法拼接字符串时:

  • concat方法的拼接逻辑是,先创建一个足以容纳待拼接的两个字符串的字节数组,然后先后将两个字符串拼到这个数组里,最后将此数组转换为字符串。
  • 在拼接大量字符串的时候,concat方法的效率低于StringBuilder。但是只拼接2个字符串时,concat方法的效率要优于StringBuilder。并且这种拼接方式代码简洁,所以只拼2个字符串时建议优先选择concat方法。

OOP

15.面向对象有哪三大特性?详细讲讲这些特性的含义

三大特性是封装、继承和多态

  1. 封装:是指将对象的数据和操作数据的方法捆绑在一起,形成一个独立的对象(类)。封装还意味着隐藏对象的内部状态和实现细节,仅通过对象提供的接口(方法)来访问对象的属性和行为。这样做的好处是提高了代码的安全性和可维护性,因为对象的内部实现可以自由更改,而不影响外部代码。
  2. 继承:继承是一种创建新类的方式,子类可以继承父类或超类的属性和方法。继承支持代码的复用,允许开发者在现有类的基础上添加新功能,而不必重写现有代码。继承还支持建立类之间的层次结构,使得代码更加有组织。
  3. 多态: 是指允许不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的实例以不同的方式实现。这意味着一个函数或者方法可以有多个不同的行为。多态使得代码更加灵活,因为可以用统一的接口来处理不同类型的对象,而具体的行为则取决于对象的实际类型。

16.在创建子类对象时类的初始化过程

第一次创建子类对象:

  1. 父类静态域
  2. 子类静态域
  3. 父类构造代码块
  4. 父类构造方法
  5. 子类构造代码块
  6. 子类构造方法

规律就是 父类先于子类 静态的先于非静态的

之后创建子类对象,父类和子类的静态代码块不会执行,其余不变

解释:

1、java的机制是先编译成字节码文件.class 然后在JVM上解释器逐行翻译成机器码文件,那从.class 到JVM加载的时候,就执行static代码块和static变量,所以先执行静态代码块,并且执行顺序是先执行父类的在执行子类的。

2、创建对象的时候,调用了它的构造方法,所有子类的构造方法第一行的时候都隐含 super(),所以会先调用父类的构造方法,而每次在执行构造方法之前都会执行代码块,父类的代码块,父类的构造方法执行完毕之后,开始执行子类的代码块,构造方法。

3、补充:

​ (1)如果有多个构造方法(比如:有参的、无参的、参数个数不同的),根据创建对象时传的参数找对应的构造方法(比如:无参的找无参的,有参的找有参的)。

​ (2)多态的顺序也是这样的。

​ (3)加上main方法

​ 子类和main方法在同一个类中,如果在new子类对象之前main方法中有语句,那么顺序就是:静态、main()的语句、父类代码块、父类构造方法……

​ 如果新建一个测试类、在new子类对象之前main方法中有语句,顺序为:main()的语句、静态、父类代码块。

​ 解释:如果子类和main在同一个类中,加载子类,静态、执行main。如果不在同一个类,先加载main的类,执行到new子类时,才加载子类。

18.为什么Java是单继承的,可以实现多个接口?

多继承会产生钻石问题(菱形继承):

  • 类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法
  • 类 D 同时继承了类 B 和类 C
  • 对于类 B、C 重写的类 A 中的方法,类 D 会继承哪一个?这里就会产生歧义
  • 考虑到这种二义性问题,Java 不支持多重继承

Java 支持类实现多接口

  • 接口中的方法是抽象的,一个类实现可以多个接口
  • 假设这些接口中存在相同方法(方法名与参数相同),在实现接口时,这个方法需要实现类来实现,并不会出现二义性的问题

19.谈谈你对super和this的理解

  • this:代表当前对象名(在程序易产生二义性之处,应该使用this来指明当前对象;如果函数的形参与类中的成员属性同名,这时需要this来指明成员属性名)
  • this()和super()都是指的是对象,所以均不可在static环境中使用
  • 从本质上讲,this是一个指向本对象的指针,而super是一个java关键字

在java中,所有的子类构造器会默认调用super(),用于初始化父类,同时当父类中存在有参构造器时,必须再提供无参构造器,否则子类构造器默认调用super()方法时就会出现找不到的报错。

20.谈谈你对重载和重写的理解

  1. 方法重载(overload):在一个类中有相同的函数名,但是参数不同,参数不同包括个数不同,顺序不同,类型不同,然后重载的返回值可以不同
  2. 方法重写(override):方法重写要求遵守两同两小一大原则:方法名相同,参数相同;子类返回类型小于等于父类方法返回类型,子类抛出异常小于等于父类方法抛出异常;子类访问权限大于等于父类方法访问权限

21.谈谈你对ThreadLocal的理解

ThreadLoca是线程局部存储TLS(thread local storage)的一种实现,是解决多线程中的对同一变量的的访问冲突的一种技术,ThreadLocal会为每一个线程维护一个和该线程绑定的变量的副本。

ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制.

它是一种采用空间换时间的技术,用副本代替加锁。threadlocal就像一个key,具体的副本值value保存在thread对象的map里面,多个线程可以通过同一个key取出自己map里面对应的value,本质上threadlocal的get和set都操作的是每个thread自己的数据,实际上threadlocal对象自身没有变化,只是每个线程把同一个threadlocal对象当做一把打开自己map的钥匙而已,多个thread是可以并发调用get和set的并且不出错。


文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录