この記事では、2023 年に選ばれた、収集する価値のある基本的な Java の高頻度面接の質問をいくつかまとめています (回答付き)。一定の参考値があるので、困っている友達が参考になれば幸いです。
オブジェクト指向の 3 つの基本特性は、カプセル化、継承、ポリモーフィズムです。
継承: 特定の型のオブジェクトが別の型のオブジェクトのプロパティを取得できるようにするメソッド。継承とは、サブクラスが親クラスの特性と動作を継承することを意味し、その結果、サブクラスのオブジェクト (インスタンス) が親クラスのインスタンス フィールドとメソッドを持つか、サブクラスが親クラスからメソッドを継承し、サブクラスが親クラスと同じ動作になります。 (推奨チュートリアル: java 入門チュートリアル)
カプセル化: 一部のオブジェクトのプロパティと実装の詳細を非表示にし、データへのアクセスは外部に公開されたインターフェイス経由でのみ可能になります。このように、オブジェクトは内部データにさまざまなレベルの保護を提供し、プログラムの無関係な部分が誤って変更されたり、オブジェクトのプライベート部分が誤って使用されたりすることを防ぎます。
ポリモーフィズム: 同じ動作でも、異なるサブクラス オブジェクトは異なる表現を持ちます。ポリモーフィズムの存在には 3 つの条件があります: 1) 継承、2) 上書き、3) 親クラスの参照がサブクラス オブジェクトを指している。
簡単な例: リーグ・オブ・レジェンドで、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 では、年齢属性はプライベートとして定義されており、外部から直接アクセスすることはできません。Cat の年齢情報を取得する唯一の方法は getAge メソッドを使用することにより、外部から年齢属性を隠します。カプセル化と呼ばれます。もちろん、ここでの年齢は単なる例であり、実際の使用ではさらに複雑なオブジェクトになる可能性があります。
// 代码块1 short s1 = 1; s1 = s1 + 1; // 代码块2 short s1 = 1; s1 += 1;
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) と同等です。興味があれば、これら 2 行のコードのバイトコードを自分でコンパイルできます。それが同じであることを確認してください。同じです。私たちが言った Java の基本的な質問が再び変わり始めました???
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 に導入されました。デフォルトでは、IntegerCache の範囲は: -128~127 です。 .この質問の 127 は IntegerCache にヒットするため、c と d は同じオブジェクトですが、128 はヒットしないため、a と b は別のオブジェクトになります。以下の図に示すように、JVM 起動パラメータ -XX:AutoBoxCacheMax=
5 、最も効率的な方法を使用して 8 の 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 および istore を通じて一時的な結果を返します。 ireturn 命令と結果。
また異常な雰囲気になるのを防ぐため、ここでは具体的なバイトコードプログラムは掲載しませんので、興味のある学生は自分でコンパイルして確認してみてください。
インターフェイスのデフォルト メソッド: Java 8 では、デフォルトのキーワードを使用するだけで、インターフェイスに非抽象メソッド実装を追加できます。
ラムダ式と関数型インターフェイス: Lambda 式は次のとおりです。本質的には匿名の内部クラス、または受け渡すことができるコードの一部である場合もあります。 Lambda では、関数をメソッドのパラメータとして使用できます (関数はパラメータとしてメソッドに渡されます)。コードをより簡潔にするために Lambda 式を使用しますが、乱用しないでください。そうしないと、可読性の問題が発生します、Josh 「Effective Java」の著者である Bloch 氏は、ラムダ式は 3 行以内で使用するのが最善であると示唆しています。
Stream API: 関数型プログラミングを使用してコレクション クラスに対して複雑な操作を実行するためのツール。ラムダ式とともに使用すると、コレクションを簡単に処理できます。 Java 8 でコレクションを操作するための主要な抽象化。コレクションに対して実行する操作を指定でき、データの検索、フィルタリング、マッピングなどの非常に複雑な操作を実行できます。 Stream API を使用してコレクション データを操作することは、SQL を使用してデータベース クエリを実行することに似ています。 Stream API を使用して操作を並行して実行することもできます。つまり、Stream API は、データを処理するための効率的で使いやすい方法を提供します。
メソッド リファレンス: メソッド リファレンスは、既存の Java クラスまたはオブジェクト (インスタンス) のメソッドまたはコンストラクターを直接参照できる非常に便利な構文を提供します。メソッド参照をラムダと組み合わせて使用すると、言語構造をよりコンパクトかつ簡潔にし、冗長なコードを減らすことができます。
日付と時刻 API: Java 8 では、日付と時刻の管理を改善するために新しい日付と時刻 API が導入されました。
オプション クラス: 有名な NullPointerException は、システム障害の最も一般的な原因です。 Google Guava プロジェクトは、null ポインター例外を解決する方法としてずっと前に Optional を導入し、null チェック コードによってコードが汚染されることを認めず、プログラマーがクリーンなコードを書くことを期待していました。 Google Guava からインスピレーションを得た Optional は、Java 8 ライブラリの一部になりました。
新しいツール: Nashorn エンジン jjs、クラス依存関係アナライザー jdeps などの新しいコンパイル ツール。
ソースが異なります: sleep() は Thread クラスから来ており、wait() は Thread クラスから来ています。オブジェクトクラスから。
同期ロックへの影響は異なります: sleep() はテーブルの同期ロックとして動作しません。現在のスレッドが同期ロックを保持している場合、スリープではスレッドが同期ロックを解放できません。 wait() は同期ロックを解放し、他のスレッドが実行のために同期されたコード ブロックに入ることができるようにします。
異なる使用範囲: sleep() はどこでも使用できます。 wait() は、同期制御メソッドまたは同期制御ブロックでのみ使用できます。それ以外の場合は、IllegalMonitorStateException がスローされます。
回復方法は異なります。2 つは現在のスレッドを一時停止しますが、回復方法は異なります。 sleep() は時間が経過すると再開します。wait() が再開するには、他のスレッドが同じオブジェクトの Notice()/nofityAll() を呼び出す必要があります。
スレッドは、sleep() メソッドの実行後にタイムアウト待ち (TIMED_WAITING) 状態になり、yield() メソッドの実行後に READY 状態になります。
sleep() メソッドは、他のスレッドに実行の機会を与えるときにスレッドの優先順位を考慮しないため、優先順位の低いスレッドに実行の機会を与えます。yield() メソッドは、同じ優先順位またはそれより高い優先順位 スレッドには実行の可能性があります。
は、現在のスレッドが終了するのを待つために使用されます。スレッド A が threadB.join() ステートメントを実行する場合、その意味は次のとおりです。現在のスレッド A は、threadB スレッドが終了するのを待ってから、threadB.join() から戻り、独自のコードの実行を継続します。
一般的に、1) Thread クラスを継承する、2) Runnable インターフェイスを実装する、3) Callable インターフェイスを実装する、という 3 つの方法があります。
その中で、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 (スレッドの最大数): スレッド プール内で開くことができるスレッドの最大数。
ハンドラー (拒否ポリシー): スレッド プールにタスクを追加するとき、拒否ポリシーは次の 2 つの状況でトリガーされます: 1) スレッド プールの実行ステータスが RUNNING ではない、2) スレッドプールがスレッドの最大数に達したためブロックされています。キューがいっぱいの場合。
keepAliveTime (キープアライブ時間): スレッド プール内の現在のスレッド数が corePoolSize を超える場合、超過したスレッドはアイドル時間が keepAliveTime を超えると終了されます。
AbortPolicy: ポリシーを中止します。デフォルトの拒否戦略は RejectedExecutionException を直接スローします。呼び出し元はこの例外をキャッチし、必要に応じて独自の処理コードを作成できます。
DiscardPolicy: ポリシーを破棄します。何もせず、単に拒否されたタスクを破棄します。
DiscardOldestPolicy: 最も古いポリシーを破棄します。ブロッキング キュー内の最も古いタスクを放棄することは、キュー内の次に実行されるタスクを実行し、拒否されたタスクを再送信することと同じです。ブロッキング キューが優先キューの場合、「最も古いものを削除」戦略により最も優先度の高いタスクが削除されるため、優先キューではこの戦略を使用しないことをお勧めします。
CallerRunsPolicy: 呼び出し元の実行ポリシー。呼び出し側スレッドでタスクを実行します。この戦略は、タスクを放棄したり例外をスローしたりせず、呼び出し元 (タスクを実行するためにスレッド プールを呼び出すメイン スレッド) にタスクをロールバックする調整メカニズムを実装します。タスクの実行には一定の時間がかかるため、そのため、メインスレッドは少なくとも一定期間タスクを送信できず、スレッド プールの時間が実行中のタスクの処理を完了することができます。
List (シーケンスを扱うための優れたヘルパー): List インターフェイスには、一意ではない (同じオブジェクトを参照する複数の要素が存在する可能性があります)、順序付けられたオブジェクトのセット。
Set (固有のプロパティに重点を置く): 重複したセットは許可されず、複数の要素が同じオブジェクトを参照することはありません。
マップ (キーを使用して検索するプロフェッショナル ユーザー): キーと値のペアのストレージを使用します。 Map は Key に関連付けられた値を保持します。 2 つのキーは同じオブジェクトを参照できますが、キーを繰り返すことはできません。一般的なキーは文字列型ですが、任意のオブジェクトにすることもできます。
ArrayList の最下層は動的配列実装に基づいており、LinkedList の最下層はリンク リスト実装に基づいています。
インデックスによるデータのインデックス付け (get/set メソッド) の場合: ArrayList はインデックスを介して配列の対応する位置にあるノードを直接見つけますが、LinkedList はヘッド ノードまたはテール ノードからターゲット ノードまでのトラバースを開始する必要があります。が見つかるので、効率の点では LinkedList よりも ArrayList の方が優れています。
ランダムな挿入と削除の場合: ArrayList はターゲット ノードの背後にノードを移動する必要があります (ノードを移動するには System.arraycopy メソッドを使用します)。一方、LinkedList は、その前にノードの next または prev 属性を変更するだけで済みます。 LinkedList は ArrayList よりも優れています。
連続した挿入と削除の場合: ArrayList はノードを移動する必要がないため、効率の点で LinkedList よりも優れています。ほとんどの場合、使用法は順次挿入であるため、実際の使用では ArrayList がよく使用されるのはこのためです。
Vector と ArrayList はほぼ同じですが、唯一の違いは、Vector はスレッド セーフを確保するためにメソッドに synchronized を使用するため、パフォーマンスの点では ArrayList の方が優れています。
同様の関係には、StringBuilder と StringBuffer、HashMap と Hashtable が含まれます。
現在 JDK 1.8 を使用しています。最下層は「配列リンク リスト赤黒ツリー」で構成されています。以下に示すように、JDK 1.8 以前は「配列リンクリスト」で構成されていました。
主な目的は、ハッシュの競合が深刻な場合 (リンク リストが長すぎる場合) の検索パフォーマンスを向上させることです。リンク リストを使用した検索パフォーマンスは O(n) ですが、赤黒ツリーを使用した場合はO(ログン)。
挿入には、デフォルトでリンク リスト ノードが使用されます。追加後に同じインデックス位置にあるノードの数が 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 の contains メソッドを削除します。
HashMap は AbstractMap クラスから継承し、Hashtable は Dictionary クラスから継承します。
プログラム カウンタ: スレッド プライベート。現在のスレッドによって実行されるバイトコードの行番号インジケーターと見なすことができる小さなメモリ空間。スレッドが Java メソッドを実行している場合、このカウンタは実行中の仮想マシンのバイトコード命令のアドレスを記録します。スレッドがネイティブ メソッドを実行している場合、カウンタ値は空です。
Java 仮想マシン スタック: スレッドはプライベートです。そのライフサイクルはスレッドのライフサイクルと同じです。仮想マシン スタックは、Java メソッド実行のメモリ モデルを記述します。各メソッドが実行されると、ローカル変数テーブル、オペランド スタック、ダイナミック リンク、メソッド出口、その他の情報を格納するスタック フレームが作成されます。各メソッドの呼び出しから実行完了までの処理は、スタックフレームを仮想マシンのスタックにプッシュしてからポップアウトするまでの処理に相当します。
ローカル メソッド スタック: スレッド プライベート。ローカル メソッド スタックと仮想マシン スタックが実行する機能は非常によく似ています。両者の唯一の違いは、仮想マシン スタックは仮想マシンに Java メソッド (つまり、バイトコード) を実行させるのに対して、ローカル メソッド スタックは使用されることです。仮想マシンからネイティブメソッドサービスへ。
Java ヒープ: スレッド共有。ほとんどのアプリケーションでは、Java ヒープは Java 仮想マシンによって管理される最大のメモリ部分です。 Java ヒープはすべてのスレッドによって共有されるメモリ領域であり、仮想マシンの起動時に作成されます。このメモリ領域の唯一の目的はオブジェクト インスタンスを保存することであり、ほとんどすべてのオブジェクト インスタンスがここにメモリを割り当てます。
メソッド領域: Javaヒープと同様、各スレッドが共有するメモリ領域で、クラス情報(構築メソッド、インタフェース定義)、定数、静的変数、ジャストインタイムなどを格納するために使用されます。仮想マシンによってロードされたコンパイラ コンパイルされたコード (バイトコード) およびその他のデータ。メソッド領域は 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的对象包括下面几种:
マーキング - 消去アルゴリズム
まず、リサイクルする必要があるすべてのオブジェクトにマークを付けます。マーキングが完了すると、マークされたすべてのオブジェクトが均一にリサイクルされます。主な欠点は 2 つあり、1 つはマーキングとクリアのプロセスの効率が高くない効率の問題、もう 1 つはスペースの問題で、マーキングとクリア後に大量の不連続なメモリ フラグメントが生成されます。将来、プログラムの実行中により大きなオブジェクトを割り当てる必要がある場合、十分な連続メモリが見つからないため、事前に別のガベージ コレクション アクションをトリガーする必要があります。
コピーアルゴリズム
効率性の問題を解決するために、利用可能なメモリを容量に応じてサイズに分割する「Copying」(コピー)と呼ばれる収集アルゴリズムが登場しました。等しいピースが 2 つあり、一度に 1 つだけを使用します。このメモリ ブロックが使い果たされると、残っているオブジェクトを別のブロックにコピーし、使用されているメモリ領域を一度にクリーンアップします。これにより、毎回半分の領域全体がリサイクルされるため、メモリを割り当てる際にメモリの断片化などの複雑な状況を考慮する必要がなく、ヒープの先頭ポインタを移動して順番にメモリを割り当てるだけで実装が簡単です。効率的に実行できます。ただ、このアルゴリズムのコストはメモリを元のサイズの半分に減らすことですが、これは少し高すぎます。
マーク - 照合アルゴリズム
コピー収集アルゴリズムは、オブジェクトの生存率が高い場合、より多くのコピー操作を実行するため、効率が低くなります。さらに重要なのは、スペースの 50% を無駄にしたくない場合は、使用済みメモリ内のすべてのオブジェクトが 100% 生きているという極端な状況に対処するために、割り当てを保証するための追加のスペースが必要であるため、この方法は通常は使用できません。旧世代のアルゴリズムで直接使用されます。
旧世代の特性に応じて、別の「mark-compact」アルゴリズムが提案されました。マーキングプロセスは「mark-clear」アルゴリズムと同じですが、その後のステップは直接関係しません。オブジェクトはクリーンアップされますが、残っているすべてのオブジェクトは一方の端に移動され、その後、端の境界の外側にあるメモリは直接クリアされます。
世代コレクション アルゴリズム
現在、商用仮想マシンはガベージ コレクションに「世代コレクション」(Generational Collection) アルゴリズムを使用しています。このアルゴリズムには新しいものはありません。オブジェクトの異なるライフサイクルに従ってメモリをいくつかのブロックに分割するだけです。
一般に、Java ヒープは新世代と旧世代に分かれており、各世代の特性に応じて最適な収集アルゴリズムが使用されます。
新世代では、ガベージ コレクション中に毎回大量のオブジェクトが消滅し、少数のオブジェクトだけが生き残ることがわかりました。その後、コピー アルゴリズムを使用すると、少額のコピー コストを支払うだけで済みます。コレクションを完了するために生き残るオブジェクトの数。
旧世代では、オブジェクトの生存率が高く、それを保証するために割り当てる余分なスペースがないため、リサイクルにはマーククリーンまたはマーククリーンアルゴリズムを使用する必要があります。
金・三・銀の季節、転職活動を準備している学生も多いと思います。
私は最近のオリジナル記事を要約しました: オリジナルの概要には、大企業との面接で遭遇した多くの高頻度の面接質問の分析が含まれています。私はそれぞれの質問を分析する際にレビューしています。一度読んだだけでは理解できないかもしれませんが、繰り返し読むことで何かを得られると思います。
プログラミング関連の知識について詳しくは、プログラミング コースをご覧ください。 !
以上が【吐血編】2023年Java基本高頻度面接質問と回答集(集)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。