1.说一说你对static关键字的理解
在Java类里只能包含成员变量、方法、内部类3种成员,都可以使用static修饰,外加代码块可以使用static修饰,以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。
类成员储存在方法区中,对象是存放在堆内存的。
对static关键字而言,有一条非常重要的规则:类成员不能访问实例成员,实例成员可以访问类成员。因为JVM的初始化顺序是先静态域,然后是非静态域,所以可能出现类成员已经初始化完成,但实例成员还没有初始化的情况,如果允许类成员访问实例成员将会引起大量错误。
16.什么是代码块
代码块又叫初始化块,属于类中的成员,即类的一部分 类似于方法,将逻辑语句封装在方法提中,通过{}定义的一段代码。但和方法不同,没有方法名,没有返回值,没有参数,只有方法体,而且不用通过对象或类显示调用,而是在加载类的时候或者创建对象的时候隐式调用。
静态代码块:使用
static关键字去定义,静态代码块中的代码会在JVM类加载时运行,且只被执行一次。构造代码块:和静态代码块的唯一区别就是没有static修饰
普通代码块:定义在方法中的代码块,一般不会这么无聊去使用它。
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来修饰一个内部类,则这个内部类就被称为静态内部类。
静态内部类需满足如下规则:
- 静态内部类可以包含静态成员,也可以包含非静态成员;
- 静态内部类不能访问外部类的实例成员,只能访问它的静态成员;
- 外部类的所有方法、初始化块都能访问其内部定义的静态内部类;
- 在外部类的外部,也可以实例化静态内部类,语法如下:
外部类.内部类 变量名 = 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的区别?
- final就是不可变的意思,可以修饰成员变量、局部变量、⽅法和类。修饰变量时,这个变量必须初始化,所以也称为常量。
- finally是异常处理的⼀部分,只能⽤在try/catch中,并且附带⼀个语句块表示这段语句⼀定会被执⾏,⽆论是否抛出异常。
- 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点。
- 是保证了线程安全,由于 String 的不可变,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。如果在多线程的环境中,我们需要对某个字符串频繁的修改,可以使用StringBuffer类。
- 节约内存,JVM里面有字符串常量池,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。
13.使用字符串时,new和””更推荐使用哪种方式?
先看看 “hello” 和 new String(“hello”) 的区别:
- 当Java程序直接使用 “hello” 的字符串直接量时,JVM将会使用常量池来管理这个字符串;
- 当使用 new String(“hello”) 时,JVM会先使用常量池来管理 “hello” 直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。
显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。
14.字符串拼接
拼接字符串有很多种方式,其中最常用的有4种,下面列举了这4种方式各自适合的场景。
- + 运算符:如果拼接的都是字符串字面量,则适合使用 + 运算符实现拼接;
- StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;
- StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;
- String类的concat方法:如果只是对两个字符串进行拼接,并且包含变量,则适合使用concat方法;
扩展阅读
采用 + 运算符拼接字符串时:
- 如果拼接的都是字符串字面量,拼接操作在编译器的编译期间就执行了,会直接把拼接后的常量放入字符串常量池中。
- 如果拼接操作包含字符串引用,那么它是在Java运行期间执行的,新创建的字符串存放在堆内存中。
采用StringBuilder/StringBuffer拼接字符串时:
- StringBuilder/StringBuffer都有字符串缓冲区,缓冲区的容量在创建对象时确定,并且默认为16。当拼接的字符串超过缓冲区的容量时,会触发缓冲区的扩容机制,即缓冲区加倍。
- 缓冲区频繁的扩容会降低拼接的性能,所以如果能提前预估最终字符串的长度,则建议在创建可变字符串对象时,放弃使用默认的容量,可以指定缓冲区的容量为预估的字符串的长度。
采用String类的concat方法拼接字符串时:
- concat方法的拼接逻辑是,先创建一个足以容纳待拼接的两个字符串的字节数组,然后先后将两个字符串拼到这个数组里,最后将此数组转换为字符串。
- 在拼接大量字符串的时候,concat方法的效率低于StringBuilder。但是只拼接2个字符串时,concat方法的效率要优于StringBuilder。并且这种拼接方式代码简洁,所以只拼2个字符串时建议优先选择concat方法。
OOP
15.面向对象有哪三大特性?详细讲讲这些特性的含义
三大特性是封装、继承和多态
- 封装:是指将对象的数据和操作数据的方法捆绑在一起,形成一个独立的对象(类)。封装还意味着隐藏对象的内部状态和实现细节,仅通过对象提供的接口(方法)来访问对象的属性和行为。这样做的好处是提高了代码的安全性和可维护性,因为对象的内部实现可以自由更改,而不影响外部代码。
- 继承:继承是一种创建新类的方式,子类可以继承父类或超类的属性和方法。继承支持代码的复用,允许开发者在现有类的基础上添加新功能,而不必重写现有代码。继承还支持建立类之间的层次结构,使得代码更加有组织。
- 多态: 是指允许不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的实例以不同的方式实现。这意味着一个函数或者方法可以有多个不同的行为。多态使得代码更加灵活,因为可以用统一的接口来处理不同类型的对象,而具体的行为则取决于对象的实际类型。
16.在创建子类对象时类的初始化过程
第一次创建子类对象:
- 父类静态域
- 子类静态域
- 父类构造代码块
- 父类构造方法
- 子类构造代码块
- 子类构造方法
规律就是 父类先于子类 静态的先于非静态的
之后创建子类对象,父类和子类的静态代码块不会执行,其余不变
解释:
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.谈谈你对重载和重写的理解
- 方法重载(overload):在一个类中有相同的函数名,但是参数不同,参数不同包括个数不同,顺序不同,类型不同,然后重载的返回值可以不同
- 方法重写(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的并且不出错。