이 기사에는 수집할 가치가 있는 2023개의 Java 기본 고빈도 인터뷰 질문(답변 포함)이 요약되어 있습니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.
객체 지향의 세 가지 기본 특성은 캡슐화, 상속 및 다형성입니다.
상속: 특정 유형의 객체가 다른 유형의 객체의 속성을 얻을 수 있도록 하는 방법입니다. 상속이란 하위 클래스가 상위 클래스의 특성과 동작을 상속하여 하위 클래스 객체(인스턴스)가 상위 클래스의 인스턴스 필드와 메소드를 가지거나, 하위 클래스가 상위 클래스의 메소드를 상속하여 하위 클래스가 다음을 갖는 것을 의미합니다. 상위 클래스와 동일한 동작. (추천 튜토리얼: Java 튜토리얼 소개)
캡슐화: 일부 객체의 속성 및 구현 세부 정보를 숨기고, 외부에 노출된 인터페이스를 통해서만 데이터에 액세스할 수 있습니다. 이런 방식으로 개체는 프로그램의 관련되지 않은 부분이 실수로 변경되거나 개체의 개인 부분을 잘못 사용하는 것을 방지하기 위해 내부 데이터에 대한 다양한 수준의 보호를 제공합니다.
다형성: 동일한 동작에 대해 서로 다른 하위 클래스 객체는 서로 다른 표현을 갖습니다. 다형성이 존재하는 데에는 세 가지 조건이 있습니다: 1) 상속, 2) 덮어쓰기, 3) 하위 클래스 객체에 대한 상위 클래스 참조 지점.
간단한 예: League of Legends에서 Q 키를 누릅니다.
동일한 이벤트가 생성됩니다. 다른 개체에서 발생하면 다른 결과가 발생합니다.
이해를 돕기 위해 또 다른 간단한 예를 들어보겠습니다. 이 예는 완전히 정확하지는 않지만 이해하는 데 도움이 될 것 같습니다.
public class Animal { // 动物 public void sleep() { System.out.println("躺着睡"); } } class Horse extends Animal { // 马 是一种动物 public void sleep() { System.out.println("站着睡"); } } class Cat extends Animal { // 猫 是一种动物 private int age; public int getAge() { return age + 1; } @Override public void sleep() { System.out.println("四脚朝天的睡"); } }
이 예에서는:
집과 고양이는 모두 동물이므로 둘 다 동물을 상속받고 동물의 수면 행동도 상속합니다.
그러나 수면 동작의 경우 House와 Cat이 다시 작성되었으며 이를 다형성이라고 합니다.
Cat에서는 age 속성이 비공개로 정의되어 있어 외부에서 직접 접근할 수 없습니다. Cat의 나이 정보를 얻는 유일한 방법은 getAge 메소드를 통하는 것입니다. 이를 외부에서 숨기는 것을 캡슐화라고 합니다. 물론 여기서 나이는 하나의 예시일 뿐 실제 사용에서는 훨씬 더 복잡한 객체일 수 있다.
// 代码块1 short s1 = 1; s1 = s1 + 1; // 代码块2 short s1 = 1; s1 += 1;
코드 블록 1을 컴파일할 때 오류가 보고됩니다. 오류 원인은 다음과 같습니다. 호환되지 않는 유형: int에서 short로 변환할 때 손실이 있을 수 있습니다.
코드 블록 2는 정상적으로 컴파일되고 실행됩니다. code block 2, 바이트코드는 다음과 같습니다.
public class com.joonwhee.open.demo.Convert { public com.joonwhee.open.demo.Convert(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_1 // 将int类型值1入(操作数)栈 1: istore_1 // 将栈顶int类型值保存到局部变量1中 2: iload_1 // 从局部变量1中装载int类型值入栈 3: iconst_1 // 将int类型值1入栈 4: iadd // 将栈顶两int类型数相加,结果入栈 5: i2s // 将栈顶int类型值截断成short类型值,后带符号扩展成int类型值入栈。 6: istore_1 // 将栈顶int类型值保存到局部变量1中 7: return }
바이트코드에는 int를 short로 변환하는 데 사용되는 i2s 명령어가 포함되어 있음을 알 수 있습니다. i2s는 int를 short로 변환하는 것입니다.
사실 s1 += 1은 s1 =.(short)(s1 + 1)과 동일합니다. 관심이 있다면 이 두 줄의 코드를 직접 컴파일해 보면
기본이 동일하다는 것을 알 수 있습니다. Java 질문이 다시 변경되기 시작합니까?
4. 다음 질문의 출력 결과를 지적하세요.public static void main(String[] args) { Integer a = 128, b = 128, c = 127, d = 127; System.out.println(a == b); System.out.println(c == d); }
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
IntegerCache는 특정 범위의 값을 캐시하기 위해 Integer에 도입되었습니다. 이 질문에서 ~127.127은 IntegerCache에 도달하므로 c와 d는 동일한 객체이지만 128은 적중되지 않으므로 a와 b는 다른 객체입니다.
하지만 이 캐시 범위는 수정될 수 있으며 일부 사람들은 모를 수도 있습니다. JVM을 통해 다음과 같이 상한값을 수정합니다.
5. 2를 계산하려면?
고급: 일반적인 상황에서는 비트 연산이 가장 높은 성능을 발휘한다고 간주할 수 있습니다. 그러나 실제로 컴파일러는 이제 "매우 똑똑"하며 많은 명령어 컴파일러가 실용적인 비트를 추구할 필요가 없습니다. 이는 코드의 가독성을 떨어뜨릴 뿐만 아니라 일부 영리한 최적화로 인해 컴파일러가 더 나은 최적화를 수행하지 못하게 될 수도 있습니다.
&&:逻辑与运算符。当运算符左右两边的表达式都为 true,才返回 true。同时具有短路性,如果第一个表达式为 false,则直接返回 false。
&:逻辑与运算符、按位与运算符。
按位与运算符:用于二进制的计算,只有对应的两个二进位均为1时,结果位才为1 ,否则为0。
逻辑与运算符:& 在用于逻辑与时,和 && 的区别是不具有短路性。所在通常使用逻辑与运算符都会使用 &&,而 & 更多的适用于位运算。
答:不是。Java 中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
基本数据类型:数据直接存储在栈上
引用数据类型区别:数据存储在堆上,栈上只存储引用地址
不行。String 类使用 final 修饰,无法被继承。
String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String 对象的生成。
StringBuffer:跟 String 类似,但是值可以被修改,使用 synchronized 来保证线程安全。
StringBuilder:StringBuffer 的非线程安全版本,没有使用 synchronized,具有更高的性能,推荐优先使用。
一个或两个。如果字符串常量池已经有“xyz”,则是一个;否则,两个。
当字符创常量池没有 “xyz”,此时会创建如下两个对象:
一个是字符串字面量 "xyz" 所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,此时该实例也是在堆中,字符串常量池只放引用。
另一个是通过 new String() 创建并初始化的,内容与"xyz"相同的实例,也是在堆中。
两个语句都会先去字符串常量池中检查是否已经存在 “xyz”,如果有则直接使用,如果没有则会在常量池中创建 “xyz” 对象。
另外,String s = new String("xyz") 还会通过 new String() 在堆里创建一个内容与 "xyz" 相同的对象实例。
所以前者其实理解为被后者的所包含。
==:运算符,用于比较基础类型变量和引用类型变量。
对于基础类型变量,比较的变量保存的值是否相同,类型不一定要相同。
short s1 = 1; long l1 = 1; // 结果:true。类型不同,但是值相同 System.out.println(s1 == l1);
对于引用类型变量,比较的是两个对象的地址是否相同。
Integer i1 = new Integer(1); Integer i2 = new Integer(1); // 结果:false。通过new创建,在内存中指向两个不同的对象 System.out.println(i1 == i2);
equals:Object 类中定义的方法,通常用于比较两个对象的值是否相等。
equals 在 Object 方法中其实等同于 ==,但是在实际的使用中,equals 通常被重写用于比较两个对象的值是否相同。
Integer i1 = new Integer(1); Integer i2 = new Integer(1); // 结果:true。两个不同的对象,但是具有相同的值 System.out.println(i1.equals(i2)); // Integer的equals重写方法 public boolean equals(Object obj) { if (obj instanceof Integer) { // 比较对象中保存的值是否相同 return value == ((Integer)obj).intValue(); } return false; }
不对。hashCode() 和 equals() 之间的关系如下:
当有 a.equals(b) == true 时,则 a.hashCode() == b.hashCode() 必然成立,
反过来,当 a.hashCode() == b.hashCode() 时,a.equals(b) 不一定为 true。
反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为反射机制。
数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中;引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。
浅拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。
深拷贝:对于基础数据类型:直接复制数据值;对于引用数据类型:开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。
深拷贝相比于浅拷贝速度较慢并且花销较大。
并发:两个或多个事件在同一时间间隔发生。
并行:两个或者多个事件在同一时刻发生。
并行是真正意义上,同一时刻做多件事情,而并发在同一时刻只会做一件事件,只是可以将时间切碎,交替做多件事情。
网上有个例子挺形象的:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。
值传递。Java 中只有值传递,对于对象参数,值的内容是对象的引用。
public class Demo { /** * 静态变量:又称类变量,static修饰 */ public static String STATIC_VARIABLE = "静态变量"; /** * 实例变量:又称成员变量,没有static修饰 */ public String INSTANCE_VARIABLE = "实例变量"; }
成员变量存在于堆内存中。静态变量存在于方法区中。
成员变量与对象共存亡,随着对象创建而存在,随着对象被回收而释放。静态变量与类共存亡,随着类的加载而存在,随着类的消失而消失。
成员变量所属于对象,所以也称为实例变量。静态变量所属于类,所以也称为类变量。
成员变量只能被对象所调用 。静态变量可以被对象调用,也可以被类名调用。
区分两种情况,发出调用时是否显示创建了对象实例。
1)没有显示创建对象实例:不可以发起调用,非静态方法只能被对象所调用,静态方法可以通过对象调用,也可以通过类名调用,所以静态方法被调用时,可能还没有创建任何实例对象。因此通过静态方法内部发出对非静态方法的调用,此时可能无法知道非静态方法属于哪个对象。
public class Demo { public static void staticMethod() { // 直接调用非静态方法:编译报错 instanceMethod(); } public void instanceMethod() { System.out.println("非静态方法"); } }
2)显示创建对象实例:可以发起调用,在静态方法中显示的创建对象实例,则可以正常的调用。
public class Demo { public static void staticMethod() { // 先创建实例对象,再调用非静态方法:成功执行 Demo demo = new Demo(); demo.instanceMethod(); } public void instanceMethod() { System.out.println("非静态方法"); } }
public class InitialTest { public static void main(String[] args) { A ab = new B(); ab = new B(); } } class A { static { // 父类静态代码块 System.out.print("A"); } public A() { // 父类构造器 System.out.print("a"); } } class B extends A { static { // 子类静态代码块 System.out.print("B"); } public B() { // 子类构造器 System.out.print("b"); } }
执行结果:ABabab,两个考察点:
1)静态变量只会初始化(执行)一次。
2)当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。
关于初始化,这题算入门题,我之前还写过一道有(fei)点(chang)意(bian)思(tai)的进阶题目,有兴趣的可以看看:一道有意思的“初始化”面试题
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。
重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
如果我们有两个方法如下,当我们调用:test(1) 时,编译器无法确认要调用的是哪个。
// 方法1 int test(int a); // 方法2 long test(int a);
方法的返回值只是作为方法运行之后的一个“状态”,但是并不是所有调用都关注返回值,所以不能将返回值作为重载的唯一区分条件。
抽象类只能单继承,接口可以多实现。
抽象类可以有构造方法,接口中不能有构造方法。
抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是 public static final)
抽象类中可以包含非抽象的方法,在 Java 7 之前接口中的所有方法都是抽象的,在 Java 8 之后,接口支持非抽象方法:default 方法、静态方法等。Java 9 支持私有方法、私有静态方法。
抽象类中的方法类型可以是任意修饰符,Java 8 之前接口中的方法只能是 public 类型,Java 9 支持 private 类型。
设计思想的区别:
接口是自上而下的抽象过程,接口规范了某些行为,是对某一行为的抽象。我需要这个行为,我就去实现某个接口,但是具体这个行为怎么实现,完全由自己决定。
抽象类是自下而上的抽象过程,抽象类提供了通用实现,是对某一类事物的抽象。我们在写实现类的时候,发现某些实现类具有几乎相同的实现,因此我们将这些相同的实现抽取出来成为抽象类,然后如果有一些差异点,则可以提供抽象方法来支持自定义实现。
我在网上看到有个说法,挺形象的:
普通类像亲爹 ,他有啥都是你的。
抽象类像叔伯,有一部分会给你,还能指导你做事的方法。
接口像干爹,可以给你指引方法,但是做成啥样得你自己努力实现。
Error 和 Exception 都是 Throwable 的子类,用于表示程序出现了不正常的情况。区别在于:
Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。
Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。
修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract 和 final。
修饰方法:该方法不能被子类重写。
修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。 如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。
public class FinalDemo { // 不可再修改该变量的值 public static final int FINAL_VARIABLE = 0; // 不可再修改该变量的引用,但是可以直接修改属性值 public static final User USER = new User(); public static void main(String[] args) { // 输出:User(id=0, name=null, age=0) System.out.println(USER); // 直接修改属性值 USER.setName("test"); // 输出:User(id=0, name=test, age=0) System.out.println(USER); } }
其实是三个完全不相关的东西,只是长的有点像。。
final 如上所示。
finally:finally 是对 Java 异常处理机制的最佳补充,通常配合 try、catch 使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到 finally 中,可以大大降低程序出错的几率。
finalize:Object 中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()方法仅作为了解即可,在 Java 9 中该方法已经被标记为废弃,并添加新的 java.lang.ref.Cleaner,提供了更灵活和有效的方法来释放资源。这也侧面说明了,这个方法的设计是失败的,因此更加不能去使用它。
public class TryDemo { public static void main(String[] args) { System.out.println(test()); } public static int test() { try { return 1; } catch (Exception e) { return 2; } finally { System.out.print("3"); } } }
执行结果:31。
相信很多同学应该都做对了,try、catch。finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。
public class TryDemo { public static void main(String[] args) { System.out.println(test1()); } public static int test1() { try { return 2; } finally { return 3; } } }
执行结果:3。
这题有点陷阱,但也不难,try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
finally 里面使用 return 仅存在于面试题中,实际开发中千万不要这么用。
public class TryDemo { public static void main(String[] args) { System.out.println(test1()); } public static int test1() { int i = 0; try { i = 2; return i; } finally { i = 3; } } }
执行结果:2。
这边估计有不少同学会以为结果应该是 3,因为我们知道在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?确实很容易这么想,我最初也是这么想的,当初的自己还是太年轻了啊。
这边的根本原因是,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使这边 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
실제로는 바이트코드를 기준으로 쉽게 알 수 있는데, 최종적으로 진입하기 전에 JVM은 iload 및 istore 명령을 사용하여 결과를 임시로 저장하고 최종 반환할 때 iload 및 ireturn 명령을 통해 임시 결과를 반환합니다. .
분위기가 다시 이상해지는 것을 방지하기 위해 여기에는 구체적인 바이트코드 프로그램을 게시하지 않겠습니다. 관심 있는 학생들은 직접 컴파일하여 확인할 수 있습니다.
인터페이스 기본 메소드: Java 8을 사용하면 인터페이스에 비추상 메소드 구현을 추가할 수 있습니다. 기본 키워드만 사용하면 됩니다.
Lambda 표현식 및 기능적 인터페이스: Lambda 표현식은 본질적으로 익명의 내부 클래스일 수도 있습니다. 전달할 수 있는 코드 조각입니다. Lambda를 사용하면 함수를 메서드의 매개 변수로 사용할 수 있습니다(함수가 메서드에 매개 변수로 전달됨). Lambda 표현식을 사용하면 코드를 더 간결하게 만들 수 있지만 남용하지 마세요. 그렇지 않으면 가독성 문제가 발생합니다. Josh "Effective Java"의 저자인 Bloch는 람다 표현식을 3줄 이하로 사용하는 것이 가장 좋다고 제안했습니다.
Stream API: 함수형 프로그래밍을 사용하여 컬렉션 클래스에 대한 복잡한 작업을 수행하는 도구로, 컬렉션을 쉽게 처리하기 위해 Lambda 표현식과 함께 사용할 수 있습니다. Java 8의 컬렉션 작업을 위한 주요 추상화입니다. 이를 통해 컬렉션에 대해 수행하려는 작업을 지정할 수 있으며 데이터 찾기, 필터링, 매핑과 같은 매우 복잡한 작업을 수행할 수 있습니다. Stream API를 사용하여 컬렉션 데이터에 대해 작업하는 것은 SQL을 사용하여 데이터베이스 쿼리를 수행하는 것과 유사합니다. Stream API를 사용하여 작업을 병렬로 수행할 수도 있습니다. 즉, Stream API는 데이터를 처리하는 효율적이고 사용하기 쉬운 방법을 제공합니다.
메서드 참조: 메서드 참조는 기존 Java 클래스나 객체(인스턴스)의 메서드나 생성자를 직접 참조할 수 있는 매우 유용한 구문을 제공합니다. 람다와 함께 사용하면 메서드 참조를 통해 언어 구조를 더욱 간결하고 간결하게 만들고 중복 코드를 줄일 수 있습니다.
날짜 및 시간 API: Java 8은 날짜 및 시간 관리를 개선하기 위해 새로운 날짜 및 시간 API를 도입합니다.
옵션 클래스: 유명한 NullPointerException은 시스템 오류의 가장 일반적인 원인입니다. Google Guava 프로젝트에서는 오래 전에 널 포인터 예외를 해결하고, 널 검사 코드로 인해 코드가 오염되는 것을 거부하고, 프로그래머가 깨끗한 코드를 작성하기를 기대하는 방법으로 Optional을 도입했습니다. Google Guava에서 영감을 받은 Optional은 이제 Java 8 라이브러리의 일부입니다.
새로운 도구: Nashorn 엔진 jjs, 클래스 종속성 분석기 jdeps와 같은 새로운 컴파일 도구.
의 차이점은 서로 다른 소스에서 비롯됩니다. sleep()은 Thread 클래스에서 오고, wait()는 Object 클래스에서 옵니다.
동기화 잠금에 미치는 영향은 다릅니다. sleep()은 테이블에서 동기화 잠금으로 작동하지 않습니다. 현재 스레드가 동기화 잠금을 보유하고 있으면 sleep은 스레드가 동기화 잠금을 해제하도록 허용하지 않습니다. wait()는 동기화 잠금을 해제하고 다른 스레드가 실행을 위해 동기화된 코드 블록에 들어갈 수 있도록 허용합니다.
다양한 사용 범위: sleep()은 어디에서나 사용할 수 있습니다. wait()는 동기화된 제어 메소드 또는 동기화된 제어 블록에서만 사용할 수 있습니다. 그렇지 않으면 IllegalMonitorStateException이 발생합니다.
복구 방법은 다릅니다. 둘 다 현재 스레드를 일시 중지하지만 복구 방법은 다릅니다. sleep()은 시간이 다 된 후에 재개됩니다. wait()는 다시 재개하려면 다른 스레드가 동일한 객체의 inform()/nofityAll()을 호출해야 합니다.
스레드는 sleep() 메서드를 실행한 후 타임아웃 대기(TIMED_WAITING) 상태에 들어가고, Yield() 메서드를 실행한 후 준비(READY) 상태에 들어갑니다.
sleep() 메서드는 다른 스레드에 실행할 기회를 줄 때 스레드의 우선순위를 고려하지 않으므로 우선순위가 낮은 스레드에게 실행할 기회를 제공합니다. 또는 실행 우선순위가 더 높습니다.
은 현재 스레드가 종료될 때까지 기다리는 데 사용됩니다. 스레드 A가 threadB.join() 문을 실행하는 경우 의미는 다음과 같습니다. 현재 스레드 A는 threadB.join()에서 반환되어 자체 코드를 계속 실행하기 전에 threadB 스레드가 종료될 때까지 기다립니다.
일반적으로 세 가지 방법이 있습니다. 1) Thread 클래스를 상속합니다. 2) Runnable 인터페이스를 구현합니다.
그 중 Thread는 실제로 Runable 인터페이스를 구현합니다. Runnable과 Callable의 주요 차이점은 반환 값이 있는지 여부입니다.
run(): 일반적인 메서드 호출은 메인 스레드에서 실행되며 실행을 위해 새 스레드를 생성하지 않습니다.
start(): 새 스레드를 시작합니다. 이때 스레드는 준비(실행 가능) 상태이며 실행 중이 아닙니다. CPU 타임 슬라이스를 얻으면 run() 메서드가 실행되기 시작합니다.
스레드는 다음 상태 중 하나일 수 있습니다.
NEW:新建但是尚未启动的线程处于此状态,没有调用 start() 方法。
RUNNABLE:包含就绪(READY)和运行中(RUNNING)两种状态。线程调用 start() 方法会会进入就绪(READY)状态,等待获取 CPU 时间片。如果成功获取到 CPU 时间片,则会进入运行中(RUNNING)状态。
BLOCKED:线程在进入同步方法/同步块(synchronized)时被阻塞,等待同步锁的线程处于此状态。
WAITING:无限期等待另一个线程执行特定操作的线程处于此状态,需要被显示的唤醒,否则会一直等待下去。例如对于 Object.wait(),需要等待另一个线程执行 Object.notify() 或 Object.notifyAll();对于 Thread.join(),则需要等待指定的线程终止。
TIMED_WAITING:在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟 WAITING 类似,区别在于该状态有超时时间参数,在超时时间到了后会自动唤醒,避免了无期限的等待。
TERMINATED:执行完毕已经退出的线程处于此状态。
线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。
1)Lock 是一个接口;synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
2)Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;synchronized 不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;
3)Lock 的使用更加灵活,可以有响应中断、有超时时间等;而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁;
4)在性能上,随着近些年 synchronized 的不断优化,Lock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用 synchronized,除非 synchronized 无法满足需求时,则可以使用 Lock。
1.作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。
public synchronized void method() {}
2.作用于静态方法,锁住的是类的Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。
public static synchronized void method() {}
3.作用于 Lock.class,锁住的是 Lock 的Class对象,也是全局只有一个。
synchronized (Lock.class) {}
4.作用于 this,锁住的是对象实例,每一个对象实例有一个锁。
synchronized (this) {}
5.作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。
public static Object monitor = new Object(); synchronized (monitor) {}
死锁的四个必要条件:
1)互斥条件:进程对所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2)请求和保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。
3)不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。
4)环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1) 占有(i=0, 1, …, n-1),Pn 等待的资源被 P0占 有,如下图所示。
预防死锁的方式就是打破四个必要条件中的任意一个即可。
1)打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。。
2)打破请求和保持条件:1)采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。
3) 양도 불가 조건 깨기: 프로세스가 특정 리소스를 점유한 후 추가로 다른 리소스를 적용했지만 이를 충족할 수 없는 경우 프로세스는 원래 점유했던 리소스를 해제해야 합니다.
4) 루프 대기 조건 깨기: 질서 있는 리소스 할당 전략을 구현하고, 시스템의 모든 리소스에 균일한 번호를 부여하며, 모든 프로세스는 일련 번호가 증가하는 형태로만 리소스를 신청할 수 있습니다.
메서드에서 직접 새 스레드를 생성하면 이 메서드를 자주 호출할 때 많은 스레드가 생성되어 시스템 리소스를 소비할 뿐만 아니라 시스템 안정성이 저하되고 실수로 시스템이 중단될 수 있습니다. 재무 부서에 직접 가서 확인할 수 있습니다.
스레드 풀을 합리적으로 사용하면 시스템 충돌이라는 딜레마를 피할 수 있습니다. 일반적으로 스레드 풀을 사용하면 다음과 같은 이점을 얻을 수 있습니다.
threadFactory: 작업자 스레드를 생성하는 데 사용되는 팩토리입니다.
corePoolSize(코어 스레드 수): 스레드 풀에 corePoolSize보다 실행 중인 스레드 수가 적으면 다른 작업자 스레드가 유휴 상태인 경우에도 요청을 처리하기 위해 새 스레드가 생성됩니다.
workQueue(큐): 작업을 유지하고 작업자 스레드에 전달하는 데 사용되는 차단 대기열입니다.
maximumPoolSize(최대 스레드 수): 스레드 풀에서 열 수 있는 최대 스레드 수입니다.
handler(거부 정책): 스레드 풀에 작업을 추가할 때 다음 두 가지 상황에서 거부 정책이 트리거됩니다. 1) 스레드 풀의 실행 상태가 RUNNING이 아닙니다. 2) 스레드 풀이 최대 스레드 수 및 차단 대기열이 가득 찼습니다.
keepAliveTime(유지 시간): 스레드 풀의 현재 스레드 수가 corePoolSize를 초과하는 경우 유휴 시간이 keepAliveTime을 초과하면 초과 스레드가 종료됩니다.
AbortPolicy: 정책을 중단합니다. 기본 거부 전략은 RejectedExecutionException을 직접 발생시킵니다. 호출자는 이 예외를 포착하고 필요에 따라 자체 처리 코드를 작성할 수 있습니다.
DiscardPolicy: 정책을 삭제합니다. 아무것도 하지 않고 거부된 작업을 삭제합니다.
DiscardOldestPolicy: 가장 오래된 정책을 삭제합니다. 차단 대기열에서 가장 오래된 작업을 포기하는 것은 대기열에서 실행할 다음 작업을 실행한 후 거부된 작업을 다시 제출하는 것과 같습니다. 차단 대기열이 우선 순위 대기열인 경우 "가장 오래된 삭제" 전략을 사용하면 우선 순위가 가장 높은 작업이 삭제되므로 우선 순위 대기열과 함께 이 전략을 사용하지 않는 것이 가장 좋습니다.
CallerRunsPolicy: 호출자 실행 정책입니다. 호출자 스레드에서 작업을 실행합니다. 이 전략은 작업을 포기하거나 예외를 발생시키지 않고 작업을 호출자(작업을 실행하기 위해 스레드 풀을 호출하는 기본 스레드)에게 롤백하는 조정 메커니즘을 구현합니다. 따라서 메인 스레드는 적어도 일정 기간 동안 작업을 제출할 수 없으므로 스레드 풀 시간이 실행 중인 작업 처리를 완료할 수 있습니다.
List(순서를 처리하는 데 유용한 도우미): List 인터페이스는 고유하지 않은 집합을 저장합니다(동일한 개체를 참조하는 여러 요소가 있을 수 있음). ), 주문된 개체.
세트(고유한 속성에 중점): 중복된 세트는 허용되지 않으며 여러 요소가 동일한 개체를 참조하지 않습니다.
지도(키를 사용하여 검색하는 전문 사용자): 키-값 쌍 저장소를 사용합니다. Map은 Key와 관련된 값을 유지합니다. 두 개의 키는 동일한 객체를 참조할 수 있지만 키는 반복될 수 없습니다. 일반적인 키는 문자열 유형이지만 임의의 객체일 수도 있습니다.
ArrayList의 하단 레이어는 동적 배열 구현을 기반으로 하며 LinkedList의 하단 레이어는 연결 목록 구현을 기반으로 합니다.
인덱스로 데이터를 인덱싱하는 경우(get/set 방식): ArrayList는 인덱스를 통해 배열의 해당 위치에 노드를 직접 찾는 반면 LinkedList는 대상 노드를 찾을 때까지 헤드 노드 또는 테일 노드에서 순회해야 하므로 LinkedList에서는 ArrayList가 효율성이 뛰어납니다.
무작위 삽입 및 삭제의 경우: ArrayList는 노드를 대상 노드 뒤로 이동해야 하며(노드를 이동하려면 System.arraycopy 메서드 사용), LinkedList는 대상 전후 노드의 다음 또는 이전 속성만 수정하면 됩니다. 노드이므로 LinkedList는 ArrayList 효율성 측면에서 LinkedList보다 낫습니다.
순차 삽입 및 삭제의 경우: ArrayList는 노드를 이동할 필요가 없으므로 효율성 측면에서 LinkedList보다 낫습니다. 이것이 ArrayList가 실제 사용에서 더 많이 사용되는 이유입니다. 대부분의 경우 순차적 삽입을 사용하기 때문입니다.
Vector와 ArrayList는 거의 동일합니다. 유일한 차이점은 Vector가 스레드 안전성을 보장하기 위해 메서드에 동기화를 사용하므로 ArrayList가 성능 측면에서 더 나은 성능을 갖는다는 것입니다.
유사한 관계에는 StringBuilder 및 StringBuffer, HashMap 및 Hashtable이 포함됩니다.
현재 JDK 1.8을 사용하고 있습니다. JDK 1.8 이전에는 아래와 같이 맨 아래 레이어가 "배열 + 연결 리스트 + 레드-블랙 트리"로 구성되었습니다. "배열" + 연결리스트" 구성으로 구성됩니다.
주로 해시 충돌이 심각한 경우(연결된 목록이 너무 긴 경우) 검색 성능을 향상시키기 위해 연결 목록을 사용한 검색 성능은 O(n)인 반면, 레드-블랙 트리를 사용한 검색 성능은 O(logn)입니다.
삽입의 경우 기본적으로 연결된 목록 노드를 사용합니다. 추가 후 동일한 인덱스 위치의 노드 수가 8개(임계값 8)를 초과하는 경우: 이때 배열 길이가 64보다 크거나 같으면 연결된 목록 노드가 레드-블랙으로 변환됩니다. 트리 노드(treeifyBin); 배열 길이가 64보다 작으면 연결된 목록이 트리거되어 레드-블랙 트리로 변환되지 않지만, 이때의 데이터 양은 여전히 상대적으로 작기 때문에 확장됩니다.
제거의 경우 제거 후 동일한 인덱스 위치의 노드 수가 6개에 도달하고 인덱스 위치의 노드가 레드-블랙 트리 노드인 경우 레드-블랙 트리 노드를 연결 리스트 노드( untreeify)가 트리거됩니다.
기본 초기 용량은 16입니다. HashMap의 용량은 2의 N제곱이어야 합니다. HashMap은 우리가 전달한 용량을 기준으로 용량보다 크거나 같은 가장 작은 2의 N제곱을 계산합니다. 예를 들어 9가 전달되면 용량은 다음과 같습니다. 16.
HashMap은 키와 값이 null일 수 있도록 허용하지만 Hashtable은 그렇지 않습니다.
HashMap의 기본 초기 용량은 16이고 Hashtable은 11입니다.
HashMap의 확장은 원본의 2배이고, Hashtable의 확장은 원본의 2배에 1을 더한 것입니다.
HashMap은 스레드로부터 안전하지 않지만 Hashtable은 스레드로부터 안전합니다.
HashMap의 해시 값이 다시 계산되었으며 Hashtable은 hashCode를 직접 사용합니다.
HashMap은 Hashtable에서 포함 메소드를 제거합니다.
HashMap은 AbstractMap 클래스에서 상속되고, Hashtable은 Dictionary 클래스에서 상속됩니다.
프로그램 카운터: 스레드 전용. 현재 스레드가 실행하는 바이트 코드의 줄 번호 표시로 간주할 수 있는 작은 메모리 공간입니다. 스레드가 Java 메서드를 실행하는 경우 이 카운터는 실행 중인 가상 머신 바이트코드 명령의 주소를 기록합니다. 스레드가 기본 메서드를 실행하는 경우 카운터 값은 비어 있습니다.
Java 가상 머신 스택: 스레드 전용. 수명주기는 스레드의 수명주기와 동일합니다. 가상 머신 스택은 Java 메소드 실행의 메모리 모델을 설명합니다. 각 메소드가 실행될 때 로컬 변수 테이블, 피연산자 스택, 동적 링크, 메소드 종료 및 기타 정보를 저장하기 위해 스택 프레임이 생성됩니다. 각 메소드의 호출부터 실행 완료까지의 과정은 스택 프레임을 가상머신 스택에 푸시(push)하고 팝아웃(pop out)하는 과정에 해당한다.
로컬 메서드 스택: 스레드 전용. 로컬 메소드 스택과 가상 머신 스택이 수행하는 기능은 매우 유사하며, 유일한 차이점은 가상 머신 스택이 가상 머신에 Java 메소드(즉, 바이트코드)를 실행하는 역할을 한다는 점입니다. 반면 로컬 메소드 스택은 사용됩니다. 가상 머신에 의해 네이티브 메소드 서비스로.
Java 힙: 스레드 공유. 대부분의 애플리케이션에서 Java 힙은 Java 가상 머신이 관리하는 가장 큰 메모리 부분입니다. Java 힙은 모든 스레드가 공유하는 메모리 영역으로 가상 머신이 시작될 때 생성됩니다. 이 메모리 영역의 유일한 목적은 객체 인스턴스를 저장하는 것이며 거의 모든 객체 인스턴스가 여기에 메모리를 할당합니다.
메서드 영역: Java 힙과 마찬가지로 각 스레드가 공유하는 메모리 영역으로 클래스 정보(구성 메서드, 인터페이스 정의), 상수, 정적 변수, Just-In-Time으로 컴파일된 코드를 저장하는 데 사용됩니다. 가상 머신에 의해 로드된 컴파일러(바이트코드) 및 기타 데이터입니다. 메소드 영역은 JVM 사양에 정의된 개념으로, 배치되는 위치에 따라 서로 다른 구현이 배치될 수 있습니다.
运行时常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
String str = new String("hello");
上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而"hello"这个字面量是放在堆中。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
启动类加载器(Bootstrap ClassLoader):
这个类加载器负责将存放在
扩展类加载器(Extension ClassLoader):
这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载
应用程序类加载器(Application ClassLoader):
这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义类加载器:
用户自定义的类加载器。
类加载的过程包括:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接。
加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。
验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况”下是数据类型的零值。
解析:将常量池内的符号引用替换为直接引用。
初始化:到了初始化阶段,才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。
在什么时候?
在触发GC的时候,具体如下,这里只说常见的 Young GC 和 Full GC。
触发Young GC:当新生代中的 Eden 区没有足够空间进行分配时会触发Young GC。
触发Full GC:
对什么?
对那些JVM认为已经“死掉”的对象。即从GC Root开始搜索,搜索不到的,并且经过一次筛选标记没有复活的对象。
做了什么?
对这些JVM认为已经“死掉”的对象进行垃圾收集,新生代使用复制算法,老年代使用标记-清除和标记-整理算法。
在Java语言中,可作为GC Roots的对象包括下面几种:
마크 - 클리어 알고리즘
먼저 재활용이 필요한 모든 객체를 표시하고, 마킹이 완료되면 표시된 모든 객체를 균일하게 재활용합니다. 두 가지 주요 단점이 있습니다. 하나는 효율성 문제이며, 마킹 및 클리어링 프로세스의 효율성은 높지 않습니다. 다른 하나는 공간 문제입니다. 마킹 및 클리어 후 많은 수의 불연속적인 메모리 조각이 생성되고 너무 많은 공간 조각이 발생할 수 있습니다. 원인 향후 프로그램이 실행되는 동안 더 큰 객체를 할당해야 할 경우, 충분한 연속 메모리를 찾을 수 없어 사전에 또 다른 가비지 수집 작업이 실행되어야 합니다.
복사 알고리즘
효율성 문제를 해결하기 위해 사용 가능한 메모리를 용량에 따라 두 개의 동일한 크기 블록으로 나누고 한 번에 하나만 사용하는 "복사"라는 수집 알고리즘이 나타났습니다. 이 메모리 블록이 부족해지면 살아남은 객체를 다른 블록에 복사한 후, 사용한 메모리 공간을 한꺼번에 정리하세요. 이렇게 하면 매번 절반 영역 전체가 재활용되므로 메모리 할당 시 메모리 조각화 등의 복잡한 상황을 고려할 필요가 없으며 힙의 최상위 포인터를 이동하여 순서대로 메모리를 할당하기만 하면 됩니다. 실행하는 것이 효율적입니다. 단지 이 알고리즘의 비용은 메모리를 원래 크기의 절반으로 줄이는 것인데, 이는 약간 너무 높습니다.
태그 - 대조 알고리즘
복사 수집 알고리즘은 객체 생존율이 높을 때 더 많은 복사 작업을 수행하게 되어 효율성이 낮아집니다. 더 중요한 것은 공간의 50%를 낭비하지 않으려면 사용된 메모리의 모든 객체가 100% 살아 있는 극단적인 상황을 처리하기 위해 할당 보장을 위한 추가 공간이 필요하므로 일반적으로 이 방법은 불가능합니다. 이전 세대 알고리즘에서 직접 사용됩니다.
구세대의 특성에 따라 누군가가 또 다른 "Mark-Compact" 알고리즘을 제안했습니다. 마킹 프로세스는 여전히 "Mark-Clear" 알고리즘과 동일하지만 후속 단계는 재활용품을 직접 청소하는 것이 아닙니다. 대신 모든 살아있는 객체는 한쪽 끝으로 이동한 다음 끝 경계 외부의 메모리는 직접 지워집니다.
세대 컬렉션 알고리즘
현재 상용 가상 머신에서는 가비지 컬렉션을 위해 "세대 컬렉션"(세대 컬렉션) 알고리즘을 사용합니다. 이 알고리즘은 개체의 다양한 수명 주기에 따라 메모리를 분할합니다. 몇 달러.
일반적으로 Java 힙은 신세대와 구세대로 나누어지므로 각 세대의 특성에 따라 가장 적합한 수집 알고리즘을 사용할 수 있습니다.
신세대에서는 가비지 수집 중에 매번 많은 수의 개체가 죽고 소수만이 살아남는 것으로 나타났습니다. 그런 다음 복제 알고리즘을 사용하고 살아남은 소수의 개체에 대한 복사 비용만 지불하면 됩니다. 컬렉션을 완성하기 위해.
구세대에서는 객체 생존율이 높고 할당을 보장할 추가 공간이 없기 때문에 재활용을 위해 mark-clean 또는 mark-clean 알고리즘을 사용해야 합니다.
골드, 쓰리, 실버 시즌을 맞이하여 많은 학생들이 이직을 준비하고 있다고 합니다.
제가 최근에 작성한 원문을 요약했습니다. 대기업 면접에서 많이 접했던 면접 질문에 대한 분석이 포함된 원본 요약입니다. 각 질문을 분석하여 더 높은 수준의 심층 분석을 제공합니다. 표준적으로, 한 번 읽는 것만으로는 완전히 이해하기 어려울 수도 있지만, 반복해서 읽으면 뭔가 얻을 수 있을 것이라고 믿습니다.
더 많은 프로그래밍 관련 지식을 보려면 프로그래밍 코스를 방문하세요! !
위 내용은 [토혈편] 2023년 자바 기초고빈도 면접 질문과 답변(모음)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!