這篇文章為大家總結一些值得收藏的2023年精選Java基礎高頻面試題(附答案)。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。
物件導向的三個基本特徵是:封裝、繼承和多型。
繼承:讓某個類型的物件取得另一個類型的物件的屬性的方法。繼承就是子類別繼承父類別的特徵和行為,使得子類別物件(實例)具有父類別的實例域和方法,或子類別從父類別繼承方法,使得子類別具有父類別相同的行為。 (推薦教學: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 都是 Animal,所以他們都繼承了 Animal,同時也從 Animal 繼承了 sleep 這個行為。
但針對 sleep 這個行為,House 和 Cat 進行了重寫,有了不同的表現形式(實作),這個我們稱為多態。
在 Cat 裡,將 age 屬性定義為 private,外界無法直接訪問,要獲取 Cat 的 age 資訊只能透過 getAge 方法,從而對外隱藏了 age 屬性,這個就叫做封裝。當然,這邊 age 只是個例子,實際使用中可能是個複雜很多的物件。
// 代码块1 short s1 = 1; s1 = s1 + 1; // 代码块2 short s1 = 1; s1 += 1;
程式碼區塊1編譯報錯,錯誤原因是:不相容的型別: 從int轉換到short可能會有損失」。
程式碼區塊2正常編譯和執行。
我們將程式碼區塊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 }
可以看到字節碼中包含了i2s 指令,該指令用於將int 轉成short。i2s 是int to short 的縮寫。
其實,s1 = 1 相當於s1 = (short)(s1 1),有興趣的可以自己編譯下這兩行程式碼的字節碼,你會發現是一摸一樣的。
說好的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); }
&&:逻辑与运算符。当运算符左右两边的表达式都为 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。
這邊其實根據字節碼可以很容易看出來,在進入finally 之前,JVM 會使用iload、istore 兩個指令,將結果暫存,在最終返回時在透過iload、ireturn 指令返回暫存的結果。
為了避免氣氛再次變態起來,我這邊就不貼具體的字節碼程式了,有興趣的同學可以自己編譯查看下。
介面預設方法:Java 8允許我們為介面添加一個非抽象的方法實現,只需要使用default關鍵字即可
Lambda 表達式和函數式介面:Lambda表達式本質上是一段匿名內部類,也可以是一段可以傳遞的程式碼。 Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞到方法中),使用Lambda 表達式使程式碼更加簡潔,但也不要濫用,否則會有可讀性等問題,《Effective Java》作者Josh Bloch 建議使用Lambda 表達式最好不要超過3行。
Stream API:用函數式程式設計方式在集合類別上進行複雜運算的工具,配合Lambda表達式可以方便的對集合進行處理。 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查找、過濾和映射資料等操作。使用Stream API 對集合資料進行操作,就類似於使用 SQL 執行的資料庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理資料的方式。
方法參考:方法參考提供了一個非常有用的語法,可以直接引用已有Java類別或物件(實例)的方法或建構器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘程式碼。
日期時間API:Java 8 引進了新的日期時間API改進了日期時間的管理。
Optional 類別:著名的 NullPointerException 是造成系統失敗最常見的原因。很久以前 Google Guava 專案引入了 Optional 作為解決空指標異常的一種方式,不贊成程式碼被 null 檢查的程式碼污染,期望程式設計師寫整潔的程式碼。受Google Guava的鼓勵,Optional 現在是Java 8庫的一部分。
新工具:新的編譯工具,如:Nashorn引擎 jjs、 類別依賴分析器 jdeps。
來源不同:sleep() 來自 Thread 類,wait() 來自 Object 類別。
對於同步鎖定的影響不同:sleep() 不會該資料表同步鎖定的行為,如果目前執行緒持有同步鎖定,那麼 sleep 是不會讓執行緒釋放同步鎖定的。 wait() 會釋放同步鎖,讓其他執行緒進入 synchronized 程式碼區塊執行。
使用範圍不同:sleep() 可以在任何地方使用。 wait() 只能在同步控制方法或同步控制區塊裡面使用,否則會拋 IllegalMonitorStateException。
恢復方式不同:兩者會暫停當前線程,但是在恢復上不太一樣。 sleep() 在時間到了之後會重新恢復;wait() 則需要其他執行緒呼叫相同物件的 notify()/nofityAll() 才能重新復原。
執行緒執行 sleep() 方法後進入逾時等待(TIMED_WAITING)狀態,而執行 yield() 方法後進入就緒(READY)狀態。
sleep() 方法給其他執行緒運行機會時不考慮執行緒的優先權,因此會給低優先權的執行緒運行的機會;yield() 方法只會給相同優先權或更高優先權的線程以運行的機會。
用於等待目前執行緒終止。如果一個執行緒A執行了 threadB.join() 語句,其意義是:當前執行緒A等待 threadB 執行緒終止之後才從 threadB.join() 回傳繼續往下執行自己的程式碼。
通常來說,可以認為有三種方式:1)繼承 Thread 類別;2)實作 Runnable 介面;3)實作 Callable 介面。
其中,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)打破環路等待條件:實現資源有序分配策略,將系統的所有資源統一編號,所有程序只能以序號遞增的形式申請資源。
如果我們在方法中直接new一個線程來處理,當這個方法被呼叫頻繁時就會創建很多線程,不僅會消耗系統資源,還會降低系統的穩定性,一不小心把系統搞崩了,就可以直接去財務結帳了。
如果我們合理的使用執行緒池,則可以避免把系統搞崩的困境。總得來說,使用執行緒池可以帶來以下幾個好處:
threadFactory(執行緒工廠):用於建立工作執行緒的工廠。
corePoolSize(核心執行緒數):當執行緒池執行的執行緒少於 corePoolSize 時,將建立一個新執行緒來處理請求,即使其他工作執行緒處於空閒狀態。
workQueue(佇列):用於保留任務並移交給工作執行緒的阻塞佇列。
maximumPoolSize(最大執行緒數):執行緒池允許開啟的最大執行緒數。
handler(拒絕策略):在執行緒池中新增任務時,將在下面兩種情況觸發拒絕策略:1)執行緒池運行狀態不是RUNNING;2)執行緒池已經達到最大執行緒數,並且阻塞隊列已滿時。
keepAliveTime(保持存活時間):如果執行緒池目前執行緒數超過 corePoolSize,則多餘的執行緒空閒時間超過 keepAliveTime 時會被終止。
AbortPolicy:中止策略。預設的拒絕策略,直接拋出 RejectedExecutionException。呼叫者可以捕獲這個異常,然後根據需求編寫自己的處理程式碼。
DiscardPolicy:拋棄策略。什麼都不做,直接拋棄被拒絕的任務。
DiscardOldestPolicy:拋棄最老策略。拋棄阻塞佇列中最老的任務,相當於就是佇列中下一個將要執行的任務,然後重新提交被拒絕的任務。如果阻塞隊列是優先隊列,那麼「拋棄最舊的」策略將導致拋棄優先順序最高的任務,因此最好不要將該策略和優先權隊列放在一起使用。
CallerRunsPolicy:呼叫者運行策略。在呼叫者執行緒中執行該任務。該策略實現了一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將任務回退到呼叫者(調用線程池執行任務的主線程),由於執行任務需要一定時間,因此主執行緒至少在一段時間內不能提交任務,這使得執行緒池有時間處理完正在執行的任務。
List(對付順序的好幫手):List 介面儲存一組不唯一(可以有多個元素引用相同的物件)、有序的物件。
Set(著重獨一無二的性質):不允許重複的集合,不會有多個元素引用相同的物件。
Map(用Key來搜尋的專業戶): 使用鍵值對儲存。 Map 會維護與 Key 有關聯的值。兩個 Key可以引用相同的對象,但 Key 不能重複,典型的 Key 是String類型,但也可以是任何對象。
ArrayList 底層基於動態數組實現,LinkedList 底層基於鍊錶實作。
對於按index 索引資料(get/set方法):ArrayList 透過index 直接定位到陣列對應位置的節點,而LinkedList需要從頭結點或尾節點開始遍歷,直到尋找目標節點,因此在效率上ArrayList 優於LinkedList。
對於隨機插入和刪除: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 之前是由「數組鍊錶」組成。
主要是為了提昇在 hash 衝突嚴重時(鍊錶過長)的尋找效能,使用鍊錶的尋找效能是 O(n),而使用紅黑樹是 O(logn)。
對於插入,預設是使用鍊錶節點。當同一個索引位置的節點在新增後超過8個(閾值8):如果此時數組長度大於等於64,則會觸發鍊錶節點轉紅黑樹節點(treeifyBin);而如果數組長度小於64,則不會觸發鍊錶轉紅黑樹,而是會進行擴容,因為此時的資料量還比較小。
對於移除,當同一個索引位置的節點在移除後達到 6 個,並且該索引位置的節點為紅黑樹節點,會觸發紅黑樹節點轉鍊錶節點(untreeify)。
預設初始容量是16。 HashMap 的容量必須是2的N次方,HashMap 會根據我們傳入的容量計算一個大於等於該容量的最小的2的N次方,例如傳 9,容量為16。
HashMap 允許 key 和 value 為 null,Hashtable 不允許。
HashMap 的預設初始容量為 16,Hashtable 為 11。
HashMap 的擴容為原來的 2 倍,Hashtable 的擴容為原來的 2 倍加 1。
HashMap 是非線程安全的,Hashtable是線程安全的。
HashMap 的 hash 值重新計算過,Hashtable 直接使用 hashCode。
HashMap 去掉了 Hashtable 中的 contains 方法。
HashMap 繼承自 AbstractMap 類,Hashtable 繼承自 Dictionary 類別。
程式計數器:執行緒私有。一塊較小的記憶體空間,可以看作目前執行緒所執行的字節碼的行號指示器。如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器字節碼指令的位址;如果正在執行的是Native方法,這個計數器值則是空。
Java虛擬機器堆疊:執行緒私有。它的生命週期與線程相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個堆疊幀用於儲存局部變數表、操作數棧、動態連結、方法出口等資訊。每一個方法從呼叫直到執行完成的過程,就對應一個堆疊幀在虛擬機器棧中入棧到出棧的過程。
本地方法堆疊:執行緒私有。本機方法棧與虛擬機棧所扮演的角色是非常相似的,它們之間的差異不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。
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的对象包括下面几种:
標記 - 清除演算法
首先標記所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式運行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
複製演算法
為了解決效率問題,一種稱為「複製」(Copying)的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。
標記 - 整理演算法
複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對像都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法.
根據老年代的特點,有人提出了另外一種「標記-整理」(Mark-Compact)演算法,標記過程仍然與「標記-清除」演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
分代收集演算法
目前商業虛擬機的垃圾收集都採用「分代收集」(Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。
通常是把Java堆分成新生代和老年代,這樣就可以根據各個年代的特徵採用最適當的收集演算法。
在新生代中,每次垃圾收集時都發現有一大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活對象的複製成本就可以完成收集。
在老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清理或標記—整理演算法來進行回收。
金三銀四的季節,相信有不少同學正準備跳槽。
我將我最近的原創的文章進行了匯總:原創匯總,其中有不少面試高頻題目解析,很多都是我自己在面試大廠時遇到的,我在對每個題目解析時都會以較高的標準進行深入剖析,可能只看一遍並不能完全明白,但是相信反覆閱讀,定能有所收穫。
更多程式設計相關知識,請造訪:程式設計課程! !
以上是【吐血整理】2023年Java 基礎高頻面試題目及答案(收藏)的詳細內容。更多資訊請關注PHP中文網其他相關文章!