剛剛經歷過秋招,看了大量的面經,順便將常見的Java常考知識點總結了一下,並根據被問到的頻率大體作了一個標註。一顆星表示知識點須要瞭解,被問到的頻率不高,面試時起碼能說個差很少。兩顆星表示被問到的頻率較高或對理解Java有着重要的做用,建議熟練掌握。三顆星表示被問到的頻率很是高,建議深刻理解並熟練掌握其相關知識,方便麪試時拓展(方便裝逼),給面試官留下個好印象。java
[toc]程序員
JVM、JRE及JDK的關係 **
JDK(Java Development Kit)是針對Java開發員的產品,是整個Java的核心,包括了Java運行環境JRE、Java工具和Java基礎類庫。面試
Java Runtime Environment(JRE)是運行JAVA程序所必須的環境的集合,包含JVM標準實現及Java核心類庫。算法
JVM是Java Virtual Machine(Java虛擬機)的縮寫,是整個java實現跨平臺的最核心的部分,可以運行以Java語言寫做的軟件程序。編程
簡單來講就是JDK是Java的開發工具,JRE是Java程序運行所需的環境,JVM是Java虛擬機.它們之間的關係是JDK包含JRE和JVM,JRE包含JVM.設計模式
JAVA語言特色 **
- Java是一種面向對象的語言
- Java經過Java虛擬機實現了平臺無關性,一次編譯,處處運行
- 支持多線程
- 支持網絡編程
- 具備較高的安全性和可靠性
JAVA和C++的區別 **
面試時記住前四個就好了數組
- Java 經過虛擬機從而實現跨平臺特性,可是 C++ 依賴於特定的平臺。
- Java 沒有指針,它的引用能夠理解爲安全指針,而 C++ 具備和 C 同樣的指針。
- Java 支持自動垃圾回收,而 C++ 須要手動回收。
- Java 不支持多重繼承,只能經過實現多個接口來達到相同目的,而 C++ 支持多重繼承。
- Java 不支持操做符重載,雖然能夠對兩個 String 對象執行加法運算,可是這是語言內置支持的操做,不屬於操 做符重載,而 C++ 能夠。
- Java 的 goto 是保留字,可是不可用,C++ 可使用 goto。
Java的基本數據類型 **
注意
String
不是基本數據類型緩存
類型 | 關鍵字 | 包裝器類型 | 佔用內存(字節)(重要) | 取值範圍 | 默認值 |
---|---|---|---|---|---|
字節型 | byte | Byte | 1 | -128(-2^7) ~ 127(2^7-1) | 0 |
短整型 | short | Short | 2 | -2^15 ~ 2^15-1 | 0 |
整型 | int | Integer | 4 | -2^31 ~ 2^31-1 | 0 |
長整型 | long | Long | 8 | -2^63 ~ 2^63-1 | 0L |
單精度浮點型 | float | Float | 4 | 3.4e-45 ~ 1.4e38 | 0.0F |
雙精度浮點型 | double | Double | 8 | 4.9e-324 ~ 1.8e308 | 0.0D |
字符型 | char | Character | 2 | '\u0000' | |
布爾型 | boolean | Boolean | 1 | true/flase | flase |
隱式(自動)類型轉換和顯示(強制)類型轉換 **
- 隱式(自動)類型轉換:從存儲範圍小的類型到存儲範圍大的類型。
byte
→short(char)
→int
→long
→float
→double
- 顯示(強制)類型轉換:從存儲範圍大的類型到存儲範圍小的類型。
double
→float
→long
→int
→short(char)
→byte
。該類類型轉換極可能存在精度的損失。
看一個經典的代碼安全
short s = 1; s = s + 1;
這是會報錯的,由於1是int
型,s+1
會自動轉換爲int
型,將int
型直接賦值給short
型會報錯。服務器
作一下修改便可避免報錯
short s = 1; s = (short)(s + 1);
或這樣寫,由於s += 1
會自動進行強制類型轉換
short s = 1; s += 1;
自動裝箱與拆箱 **
-
裝箱:將基本類型用包裝器類型包裝起來
-
拆箱:將包裝器類型轉換爲基本類型
這個地方有不少易混淆的地方,但在面試中問到的頻率通常,筆試的選擇題中常常出現,還有一個
String
建立對象和這個比較像,很容易混淆,在下文能夠看到 -
下面這段代碼的輸出結果是什麼?
public class Main { public static void main(String[] args) { Integer a = 100; Integer b = 100; Integer c = 128; Integer d = 128; System.out.println(a==b); System.out.println(c==d); } }
true false
不少人看到這個結果會很疑惑,爲何會是一個
true
一個flase
.其實從源碼中能夠很容易找到緣由.首先找到Integer
方法中的valueOf
方法public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
能夠看到當不知足
if
語句中的條件,就會從新建立一個對象返回,那結果必然不相等。繼續打開IntegerCache
能夠看到private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
代碼挺長,大概說的就是在經過
valueOf
方法建立Integer
對象的時候,若是數值在[-128,127]之間,便返回指向IntegerCache.cache
中已經存在的對象的引用;不然建立一個新的Integer
對象。因此上面代碼中a
與b
相等,c
與d
不相等。 -
在看下面的代碼會輸出什麼
public class Main { public static void main(String[] args) { Double a = 1.0; Double b = 1.0; Double c = 2.0; Double d = 2.0; System.out.println(a==b); System.out.println(c==d); } }
flase flase
採用一樣的方法,能夠看到
Double
的valueOf
方法,每次返回都是從新new
一個新的對象,因此上面代碼中的結果都不想等。public static Double valueOf(double d) { return new Double(d); }
-
最後再看這段代碼的輸出結果
public class Main { public static void main(String[] args) { Boolean a = false; Boolean b = false; Boolean c = true; Boolean d = true; System.out.println(a==b); System.out.println(c==d); } }
true true
老方法繼續看
valueOf
方法public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }
再看看
TRUE
和FALSE
是個什麼東西,是兩個靜態成員屬性。public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false);
**說下結論 **:Integer
、Short
、Byte
、Character
、Long
這幾個類的valueOf
方法的實現是相似的。Double
、Float
的valueOf
方法的實現是相似的。而後是Boolean
的valueOf
方法是單獨一組的。
-
Integer i = new Integer(xxx)
和Integer i =xxx
的區別這二者的區別主要是第一種會觸發自動裝箱,第兩者不會
最後看看下面這段程序的輸出結果
public class Main { public static void main(String[] args) { Integer a = 1; Integer b = 2; Integer c = 3; Long g = 3L; int int1 = 12; int int2 = 12; Integer integer1 = new Integer(12); Integer integer2 = new Integer(12); Integer integer3 = new Integer(1); System.out.println("c==(a+b) ->"+ (c==(a+b))); System.out.println("g==(a+b) ->" + (g==(a+b))); System.out.println( "c.equals(a+b) ->" + (c.equals(a+b))); System.out.println( "g.equals(a+b) ->" + (g.equals(a+b))); System.out.println("int1 == int2 -> " + (int1 == int2)); System.out.println("int1 == integer1 -> " + (int1 == integer1)); System.out.println("integer1 == integer2 -> " + (integer1 == integer2)); System.out.println("integer3 == a1 -> " + (integer3 == a)); } }
c==(a+b) ->true g==(a+b) ->true c.equals(a+b) ->true g.equals(a+b) ->false int1 == int2 -> true int1 == integer1 -> true integer1 == integer2 -> false integer3 == a1 -> false
下面簡單解釋這些結果。
1.當 "=="運算符的兩個操做數都是包裝器類型的引用,則是比較指向的是不是同一個對象,而若是其中有一個操做數是表達式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)。因此
c==a+b
,g==a+b
爲true
。2.而對於
equals
方法會先觸發自動拆箱過程,再觸發自動裝箱過程。也就是說a+b,會先各自調用intValue
方法,獲得了加法運算後的數值以後,便調用Integer.valueOf
方法,再進行equals
比較。因此c.equals(a+b)
爲true
。而對於g.equals(a+b)
,a+b
會先拆箱進行相加運算,在裝箱進行equals
比較,可是裝箱後爲Integer
,g
爲Long
,因此g.equals(a+b)
爲false
。3.
int1 == int2
爲true
無需解釋,int1 == integer1
,在進行比較時,integer1
會先進行一個拆箱操做變成int
型在進行比較,因此int1 == integer1
爲true
。4.
integer1 == integer2
->false
。integer1
和integer2
都是經過new
關鍵字建立的,能夠當作兩個對象,因此integer1 == integer2
爲false
。integer3 == a1
->false
,integer3
是一個對象類型,而a1
是一個常量它們存放內存的位置不同,因此integer3 == a1
爲false
,具體緣由可學習下java的內存模型。
String(不是基本數據類型)
String的不可變性 ***
在 Java 8 中,String
內部使用 char
數組存儲數據。而且被聲明爲final
,所以它不可被繼承。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; }
爲何Strin
g`要設計成不可變的呢(不可變性的好處):
1.能夠緩存 hash
值()
由於 String
的hash
值常常被使用,例如 String
用作 HashMap
的 key
。不可變的特性可使得 hash
值也不可變, 所以只須要進行一次計算。
2.常量池優化
String
對象建立以後,會在字符串常量池中進行緩存,若是下次建立一樣的對象時,會直接返回緩存的引用。
3.線程安全
String
不可變性天生具有線程安全,能夠在多個線程中安全地使用。
字符型常量和字符串常量的區別 *
- 形式上: 字符常量是單引號引發的一個字符 字符串常量是雙引號引發的若干個字符
- 含義上: 字符常量至關於一個整形值(ASCII值),能夠參加表達式運算 字符串常量表明一個地址值(該字符串在內存中存放位置)
- 佔內存大小 字符常量佔兩個字節 字符串常量佔若干個字節(至少一個字符結束標誌)
什麼是字符串常量池?*
字符串常量池位於堆內存中,專門用來存儲字符串常量,能夠提升內存的使用率,避免開闢多塊空間存儲相同的字符串,在建立字符串時 JVM 會首先檢查字符串常量池,若是該字符串已經存在池中,則返回它的引用,若是不存在,則實例化一個字符串放到池中,並返回其引用。
String 類的經常使用方法都有那些?**
面試時通常不會問,但面試或筆試寫字符串相關的算法題常常會涉及到,仍是得背一背(如下大體是按使用頻率優先級排序)
length()
:返回字符串長度charAt()
:返回指定索引處的字符substring()
:截取字符串trim()
:去除字符串兩端空白split()
:分割字符串,返回一個分割後的字符串數組。replace()
:字符串替換。indexOf()
:返回指定字符的索引。toLowerCase()
:將字符串轉成小寫字母。toUpperCase()
:將字符串轉成大寫字符。
String和StringBuffer、StringBuilder的區別是什麼?***
1.可變性
String
不可變,StringBuilder
和StringBuffer
是可變的
2.線程安全性
String
因爲是不可變的,因此線程安全。StringBuffer
對方法加了同步鎖或者對調用的方法加了同步鎖,因此是線程安全的。 StringBuilder
並無對方法進行加同步鎖,因此是非線程安全的。
3.性能
StringBuilder
> StringBuffer
> String
爲了方便記憶,總結以下
是否可變 | 是否安全 | 性能 | |
---|---|---|---|
String | 不可變 | 安全 | 低 |
StringBuilder | 可變 | 不安全 | 高 |
StringBuffer | 可變 | 安全 | 較高 |
switch 是否能做用在 byte 上,是否能做用在 long 上,是否能做用在 String 上 *
switch
能夠做用於char
byte
short
int
及它們對應的包裝類型,switch
不可做用於long
double
float
boolean
及他們的包裝類型。在 JDK1.5以後能夠做用於枚舉類型,在JDK1.7以後可做用於String
類型。
Java語言採用何種編碼方案?有何特色?*
Java語言採用Unicode編碼標準,它爲每一個字符制訂了一個惟一的數值,所以在任何的語言,平臺,程序均可以放心的使用。
訪問修飾符 **
在Java編程語言中有四種權限訪問控制符,這四種訪問權限的控制符可以控制類中成員的可見性。其中類有兩種public
、default
。而方法和變量有 4 種:public
、default
、protected
、private
。
-
public : 對全部類可見。使用對象:類、接口、變量、方法
-
protected : 對同一包內的類和全部子類可見。使用對象:變量、方法。 注意:不能修飾類(外部類)。
-
default : 在同一包內可見,不使用任何修飾符。使用對象:類、接口、變量、方法。
-
private : 在同一類內可見。使用對象:變量、方法。 注意:不能修飾類(外部類)
修飾符 當前類 同包內 子類(同包) 其餘包 public Y Y Y Y protected Y Y Y N default Y Y Y N private Y N N N
運算符 *
-
&&和&
&&
和&
均可以表示邏輯與,但他們是有區別的,共同點是他們兩邊的條件都成立的時候最終結果纔是true
;不一樣點是&&
只要是第一個條件不成立爲false
,就不會再去判斷第二個條件,最終結果直接爲false
,而&
判斷的是全部的條件。 -
||和|
||
和|
都表示邏輯或,共同點是隻要兩個判斷條件其中有一個成立最終的結果就是true
,區別是||
只要知足第一個條件,後面的條件就再也不判斷,而|
要對全部的條件進行判斷。
關鍵字
static關鍵字 ***
static
關鍵字的主要用途就是方便在沒有建立對象時調用方法和變量和優化程序性能
1.static變量(靜態變量)
用static
修飾的變量被稱爲靜態變量,也被稱爲類變量,能夠直接經過類名來訪問它。靜態變量被全部的對象共享,在內存中只有一個副本,僅當在類初次加載時會被初始化,而非靜態變量在建立對象的時候被初始化,而且存在多個副本,各個對象擁有的副本互不影響。
2.static方法(靜態方法)
static
方法不依賴於任何對象就能夠進行訪問,在static
方法中不能訪問類的非靜態成員變量和非靜態成員方法,由於非靜態成員方法/變量都是必須依賴具體的對象纔可以被調用,可是在非靜態成員方法中是能夠訪問靜態成員方法/變量的。
public class Main { public static String s1 = "s1";//靜態變量 String s2 = "s2"; public void fun1(){ System.out.println(s1); System.out.println(s2); } public static void fun2(){ System.out.println(s1); System.out.println(s2);//此處報錯,靜態方法不能調用非靜態變量 } }
3.static代碼塊(靜態代碼塊)
靜態代碼塊的主要用途是能夠用來優化程序的性能,由於它只會在類加載時加載一次,不少時候會將一些只須要進行一次的初始化操做都放在static
代碼塊中進行。若是程序中有多個static
塊,在類初次被加載的時候,會按照static
塊的順序來執行每一個static
塊。
public class Main { static { System.out.println("hello,word"); } public static void main(String[] args) { Main m = new Main(); } }
4.能夠經過this訪問靜態成員變量嗎?(能夠)
this
表明當前對象,能夠訪問靜態變量,而靜態方法中是不能訪問非靜態變量,也不能使用this
引用。
5.初始化順序
靜態變量和靜態語句塊優先於實例變量和普通語句塊,靜態變量和靜態語句塊的初始化順序取決於它們在代碼中的順序。若是存在繼承關係的話,初始化順序爲父類中的靜態變量和靜態代碼塊——子類中的靜態變量和靜態代碼塊——父類中的實例變量和普通代碼塊——父類的構造函數——子類的實例變量和普通代碼塊——子類的構造函數
final 關鍵字 ***
final
關鍵字主要用於修飾類,變量,方法。
- 類:被
final
修飾的類不能夠被繼承 - 方法:被
final
修飾的方法不能夠被重寫 - 變量:被
final
修飾的變量是基本類型,變量的數值不能改變;被修飾的變量是引用類型,變量便不能在引用其餘對象,可是變量所引用的對象自己是能夠改變的。
public class Main { int a = 1; public static void main(String[] args) { final int b = 1; b = 2;//報錯 final Main m = new Main(); m.a = 2;//不報錯,能夠改變引用類型變量所指向的對象 } }
final finally finalize區別 ***
final
主要用於修飾類,變量,方法finally
通常做用在try-catch
代碼塊中,在處理異常的時候,一般咱們將必定要執行的代碼方法finally
代碼塊 中,表示無論是否出現異常,該代碼塊都會執行,通常用來存放一些關閉資源的代碼。finalize
是一個屬於Object
類的一個方法,該方法通常由垃圾回收器來調用,當咱們調用System.gc()
方法的時候,由垃圾回收器調用finalize()
,回收垃圾,但Java語言規範並不保證inalize
方法會被及時地執行、並且根本不會保證它們會被執行。
this關鍵字 **
重點掌握前三種便可
1.this
關鍵字可用來引用當前類的實例變量。主要用於形參與成員名字重名,用this
來區分。
public Person(String name, int age) { this.name = name; this.age = age; }
2.this
關鍵字可用於調用當前類方法。
public class Main { public void fun1(){ System.out.println("hello,word"); } public void fun2(){ this.fun1();//this可省略 } public static void main(String[] args) { Main m = new Main(); m.fun2(); } }
3.this()
能夠用來調用當前類的構造函數。(注意:this()
必定要放在構造函數的第一行,不然編譯不經過)
class Person{ private String name; private int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } }
4.this
關鍵字可做爲調用方法中的參數傳遞。
5.this
關鍵字可做爲參數在構造函數調用中傳遞。
6.this
關鍵字可用於從方法返回當前類的實例。super
super關鍵字 **
1.super
能夠用來引用直接父類的實例變量。和this
相似,主要用於區分父類和子類中相同的字段
2.super
能夠用來調用直接父類構造函數。(注意:super()
必定要放在構造函數的第一行,不然編譯不經過)
3.super
能夠用來調用直接父類方法。
public class Main { public static void main(String[] args) { Child child = new Child("Father","Child"); child.test(); } } class Father{ protected String name; public Father(String name) { this.name = name; } public void Say(){ System.out.println("hello,child"); } } class Child extends Father{ private String name; public Child(String name1, String name2) { super(name1); //調用直接父類構造函數 this.name = name2; } public void test(){ System.out.println(this.name); System.out.println(super.name); //引用直接父類的實例變量 super.Say(); //調用直接父類方法 } }
this與super的區別 **
-
相同點:
super()
和this()
都必須在構造函數的第一行進行調用,不然就是錯誤的this()
和super()
都指的是對象,因此,均不能夠在static
環境中使用。
-
不一樣點:
super()
主要是對父類構造函數的調用,this()
是對重載構造函數的調用super()
主要是在繼承了父類的子類的構造函數中使用,是在不一樣類中的使用;this()
主要是在同一類的不一樣構造函數中的使用
break ,continue ,return 的區別及做用 **
break
結束當前的循環體continue
結束本次循環,進入下一次循環return
結束當前方法
面向對象和麪向過程的區別 **
-
面向過程
優勢:性能比面向對象高,由於類調用時須要實例化,開銷比較大,比較消耗資源。
缺點:沒有面向對象易維護、易複用、易擴展
-
面向對象
優勢:易維護、易複用、易擴展,因爲面向對象有封裝、繼承、多態性的特性,能夠設計出低耦合的系統,使系統更加靈活、更加易於維護
缺點:性能比面向過程低
面向對象三大特性(封裝、繼承、多態) ***
-
封裝
封裝就是隱藏對象的屬性和實現細節,僅對外公開接口,控制在程序中屬性的讀和修改的訪問級別。
-
繼承
繼承就是子類繼承父類的特徵和行爲,使得子類對象(實例)具備父類的實例域和方法,或子類從父類繼承方法,使得子類具備父類相同的行爲。
-
多態(重要)
多態是同一個行爲具備多個不一樣表現形式或形態的能力。這句話不是很好理解,能夠看這個解釋,在Java語言中,多態就是指程序中定義的引用變量所指向的具體類型和經過該引用變量發出的方法調用在編程時並不肯定,而是在程序運行期間才肯定,即一個引用變量倒底會指向哪一個類的實例對象,該引用變量發出的方法調用究竟是哪一個類中實現的方法,必須在由程序運行期間才能決定。
在Java中實現多態的三個必要條件:繼承、重寫、向上轉型。繼承和重寫很好理解,向上轉型是指在多態中須要將子類的引用賦給父類對象。
public class Main { public static void main(String[] args) { Person person = new Student(); //向上轉型 person.run(); } } class Person { public void run() { System.out.println("Person"); } } class Student extends Person { //繼承 @Override public void run() { //重載 System.out.println("Student"); } }
運行結果爲
Student
面向對象五大基本原則是什麼 **
-
單一職責原則(Single-Resposibility Principle)
**一個類,最好只作一件事,只有一個引發它的變化。**單一職責原則能夠看作是低耦合、高內聚在面向對象原則上的引伸,將職責定義爲引發變化的緣由,以提升內聚性來減小引發變化的緣由。
-
開放封閉原則(Open-Closed principle)
軟件實體應該是可擴展的,而不可修改的。也就是,對擴展開放,對修改封閉的。
-
里氏替換原則 (Liskov-Substituion Principle)
**子類必須可以替換其基類。**這一思想體現爲對繼承機制的約束規範,只有子類可以替換基類時,才能保證系統在運行期內識別子類,這是保證繼承複用的基礎。在父類和子類的具體行爲中,必須嚴格把握繼承層次中的關係和特徵,將基類替換爲子類,程序的行爲不會發生任何變化。同時,這一約束反過來則是不成立的,子類能夠替換基類,可是基類不必定能替換子類。
-
依賴倒置原則(Dependecy-Inversion Principle)
**依賴於抽象。**具體而言就是高層模塊不依賴於底層模塊,兩者都同依賴於抽象;抽象不依賴於具體,具體依賴於抽象。
-
接口隔離原則(Interface-Segregation Principle)
使用多個小的專門的接口,而不要使用一個大的總接口。
抽象類和接口的對比 ***
在Java語言中,abstract class
和interface
是支持抽象類定義的兩種機制。抽象類:用來捕捉子類的通用特性的。接口:抽象方法的集合。
相同點:
- 接口和抽象類都不能實例化
- 都包含抽象方法,其子類都必須覆寫這些抽象方法
不一樣點:
類型 | 抽象類 | 接口 |
---|---|---|
定義 | abstract class | Interface |
實現 | extends(須要提供抽象類中全部聲明的方法的實現) | implements(須要提供接口中全部聲明的方法的實現) |
繼承 | 抽象類能夠繼承一個類和實現多個接口;子類只能夠繼承一個抽象類 | 接口只能夠繼承接口(一個或多個);子類能夠實現多個接口 |
訪問修飾符 | 抽象方法能夠有public、protected和default這些修飾符 | 接口方法默認修飾符是public。你不可使用其它修飾符 |
構造器 | 抽象類能夠有構造器 | 接口不能有構造器 |
字段聲明 | 抽象類的字段聲明能夠是任意的 | 接口的字段默認都是 static 和 final 的 |
在Java中定義一個不作事且沒有參數的構造方法的做用 *
Java程序存在繼承,在執行子類的構造方法時,若是沒有用super()
來調用父類特定的構造方法,則會調用父類中「沒有參數的構造方法」。若是父類只定義了有參數的構造函數,而子類的構造函數沒有用super
調用父類那個特定的構造函數,就會出錯。
在調用子類構造方法以前會先調用父類沒有參數的構造方法,其目的是 *
幫助子類作初始化工做。
一個類的構造方法的做用是什麼?若一個類沒有聲明構造方法,改程序能正確執行嗎?爲何? *
主要做用是完成對類對象的初始化工做。能夠執行。由於一個類即便沒有聲明構造方法也會有默認的不帶參數的構造方法。
構造方法有哪些特性? **
- 方法名稱和類同名
- 不用定義返回值類型
- 不能夠寫
retrun
語句 - 構造方法能夠被重載
變量 **
-
類變量:獨立於方法以外的變量,用
static
修飾。 -
實例變量:獨立於方法以外的變量,不過沒有
static
修飾。 -
局部變量:類的方法中的變量。
-
成員變量:成員變量又稱全局變量,可分爲類變量和實例變量,有
static
修飾爲類變量,沒有static
修飾爲實例變量。類變量 實例變量 局部變量 定義位置 類中,方法外 類中,方法外 方法中 初始值 有默認初始值 有默認初始值 無默認初始值 存儲位置 方法區 堆 棧 生命週期 類什麼時候被加載和卸載 實例什麼時候被建立及銷燬 方法什麼時候被調用及結束調用
內部類 **
內部類包括這四種:成員內部類、局部內部類、匿名內部類和靜態內部類
-
成員內部類
1.成員內部類定義爲位於另外一個類的內部,成員內部類能夠無條件訪問外部類的全部成員屬性和成員方法(包括
private
成員和靜態成員)。class Outer{ private double a = 0; public static int b =1; public Outer(double a) { this.a = a; } class Inner { //內部類 public void fun() { System.out.println(a); System.out.println(b); } } }
2.當成員內部類擁有和外部類同名的成員變量或者方法時,即默認狀況下訪問的是成員內部類的成員。若是要訪問外部類的同名成員,須要如下面的形式進行訪問:外部類.
this
.成員變量3.在外部類中若是要訪問成員內部類的成員,必須先建立一個成員內部類的對象,再經過指向這個對象的引用來訪問。
4.成員內部類是依附外部類而存在的,若是要建立成員內部類的對象,前提是必須存在一個外部類的對象。建立成員內部類對象的通常方式以下:
class Outter{ private double a = 0; public static int b =1; public Outter(){} public Outter(double a) { this.a = a; Inner inner = new Inner(); inner.fun(); //調用內部類的方法 } class Inner { //內部類 int b = 2; public void fun() { System.out.println(a); System.out.println(b); //訪問內部類的b System.out.println(Outter.this.b);//訪問外部類的b } } } public class Main{ public static void main(String[] args) { Outter outter = new Outter(); Outter.Inner inner = outter.new Inner(); //建立內部類的對象 } }
-
局部內部類
局部內部類是定義在一個方法或者一個做用域裏面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該做用域內。定義在實例方法中的局部類能夠訪問外部類的全部變量和方法,定義在靜態方法中的局部類只能訪問外部類的靜態變量和方法。
class Outter { private int outter_a = 1; private static int static_b = 2; public void test1(){ int inner_c =3; class Inner { private void fun(){ System.out.println(outter_a); System.out.println(static_b); System.out.println(inner_c); } } Inner inner = new Inner(); //建立局部內部類 inner.fun(); } public static void test2(){ int inner_d =3; class Inner { private void fun(){ System.out.println(outter_a); //編譯錯誤,定義在靜態方法中的局部類不能夠訪問外部類的實例變量 System.out.println(static_b); System.out.println(inner_d); } } Inner inner = new Inner(); inner.fun(); } }
-
匿名內部類
匿名內部類只沒有名字的內部類,在平常開發中使用較多。使用匿名內部類的前提條件:必須繼承一個父類或實現一個接口。
interface Person { public void fun(); } class Demo { public static void main(String[] args) { new Person() { public void fun() { System.out.println("hello,word"); } }.fun(); } }
-
靜態內部類
靜態內部類也是定義在另外一個類裏面的類,只不過在類的前面多了一個關鍵字
static
。靜態內部類是不須要依賴於外部類的,而且它不能使用外部類的非static
成員變量或者方法,這點很好理解,由於在沒有外部類的對象的狀況下,能夠建立靜態內部類的對象,若是容許訪問外部類的非static
成員就會產生矛盾,由於外部類的非static成員必須依附於具體的對象。class Outter { int a = 1; static int b = 2; public Outter() { } static class Inner { public Inner() { System.out.println(a);//報錯,靜態內部類不能訪問非靜態變量 System.out.println(b); } } } public class Main{ public static void main(String[] args) { Outter.Inner inner = new Outter.Inner(); } }
-
內部類的優勢:
- 內部類不爲同一包的其餘類所見,具備很好的封裝性;
- 匿名內部類能夠很方便的定義回調。
- 每一個內部類都能獨立的繼承一個接口的實現,因此不管外部類是否已經繼承了某個(接口的)實現,對於內部類都沒有影響。
- 內部類有效實現了「多重繼承」,優化 java 單繼承的缺陷。
-
局部內部類和匿名內部類訪問局部變量的時候,爲何變量必需要加上
final
?public class Main { public static void main(String[] args) { } public void fun(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); System.out.println(b); }; }.start(); } }
對於變量
a
能夠從生命週期的角度理解,局部變量直接存儲在棧中,當方法執行結束後,非final
的局部變量就被銷燬,而局部內部類對局部變量的引用依然存在,若是局部內部類要調用沒有final
修飾的局部變量時,就會形成生命週期不一致出錯。對於變量
b
,實際上是將fun
方法中的變量b
以參數的形式對匿名內部類中的拷貝(變量b
的拷貝)進行賦值初始化。在run
方法中訪問的變量b
根本就不是test
方法中的局部變量b
,而是一個拷貝值,因此不存在生命週期不一致的問題,但若是在run
方法中修改變量b
的值會致使數據不一致,因此須要加final
修飾。
重寫與重載 ***
重載和重寫的區別
- 重載:發生在同一個類中,方法名相同參數列表不一樣(參數類型不一樣、個數不一樣、順序不一樣),與方法返回值和訪問修飾符無關,即重載的方法不能根據返回類型進行區分。
- 重寫:發生在父子類中,方法名、參數列表必須相同,返回值小於等於父類,拋出的異常小於等於父類,訪問修飾符大於等於父類(里氏代換原則);若是父類方法訪問修飾符爲
private
則子類中就不是重寫。
構造器(constructor)是否可被重寫(override)
構造器能夠被重載,不能被重寫
重載的方法可否根據返回類型進行區分?爲何?
不能,由於調用時不能指定類型信息,編譯器不知道你要調用哪一個函數。
== 和 equals 的區別 ***
-
==
對於基本數據類型,
==
比較的是值;對於引用數據類型,==
比較的是內存地址。 -
eauals
對於沒有重寫
equals
方法的類,equals
方法和==
做用相似;對於重寫過equals
方法的類,equals
比較的是值。
hashCode 與 equals(爲何重寫equals方法後,hashCode方法也必須重寫) ***
-
equals
先看下
String
類中重寫的equals
方法。public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
從源碼中能夠看到:
equals
方法首先比較的是內存地址,若是內存地址相同,直接返回true
;若是內存地址不一樣,再比較對象的類型,類型不一樣直接返回false
;類型相同,再比較值是否相同;值相同返回true
,值不一樣返回false
。總結一下,equals
會比較內存地址、對象類型、以及值,內存地址相同,equals
必定返回true
;對象類型和值相同,equals
方法必定返回true
。- 若是沒有重寫
equals
方法,那麼equals
和==
的做用相同,比較的是對象的地址值。
-
hashCode
hashCode
方法返回對象的散列碼,返回值是int
類型的散列碼。散列碼的做用是肯定該對象在哈希表中的索引位置。關於
hashCode
有一些約定:- 兩個對象相等,則
hashCode
必定相同。 - 兩個對象有相同的
hashCode
值,它們不必定相等。 hashCode()
方法默認是對堆上的對象產生獨特值,若是沒有重寫hashCode()
方法,則該類的兩個對象的hashCode
值確定不一樣
- 兩個對象相等,則
-
爲何重寫
equals
方法後,hashCode
方法也必須重寫- 根據規定,兩個對象相等,
hashCode
值也許相同,因此重寫equals
方法後,hashCode
方法也必須重寫(面試官確定不是想聽這個答案) hashCode
在具備哈希機制的集合中起着很是關鍵的做用,好比HashMap
、HashSet
等。以HashSet
爲例,HashSet
的特色是存儲元素時無序且惟一,在向HashSet
中添加對象時,首相會計算對象的HashCode
值來肯定對象的存儲位置,若是該位置沒有其餘對象,直接將該對象添加到該位置;若是該存儲位置有存儲其餘對象(新添加的對象和該存儲位置的對象的HashCode
值相同),調用equals
方法判斷兩個對象是否相同,若是相同,則添加對象失敗,若是不相同,則會將該對象從新散列到其餘位置。因此重寫equals
方法後,hashCode
方法不重寫的話,會致使全部對象的HashCode
值都不相同,都能添加成功,那麼HashSet
中會出現不少重複元素,HashMap
也是同理(由於HashSet
的底層就是經過HashMap
實現的),會出現大量相同的Key
(HashMap
中的key
是惟一的,但不一樣的key
能夠對應相同的value
)。因此重寫equals
方法後,hashCode
方法也必須重寫。同時由於兩個對象的hashCode
值不一樣,則它們必定不相等,因此先計算對象的hashCode
值能夠在必定程度上判斷兩個對象是否相等,提升了集合的效率。總結一下,一共兩點:第一,在HashSet
等集合中,不重寫hashCode
方法會致使其功能出現問題;第二,能夠提升集合效率。
- 根據規定,兩個對象相等,
Java 中是值傳遞仍是引用傳遞,仍是二者共存 **
這是一個很容易搞混又很難解釋清楚的問題,先說結論,Java中只有值傳遞
先看這樣一段代碼
public class Main{ public static void main(String[] args) { int a = 1; printValue(a); System.out.println("a:" + a); } public static void printValue(int b){ b = 2; System.out.println("b:"+ b); } }
輸出
b:2 a:1
能夠看到將a
的值傳到printValue
方法中,並將其值改成2。但方法調用結束後,a
的值仍是1,並未發生改變,因此這種狀況下爲值傳遞。
再看這段代碼
public class Main{ public static void main(String[] args) { Preson p = new Preson(); p.name = "zhangsan"; printValue(p); System.out.println("p.name: " + p.name); } public static void printValue(Preson q){ q.name = "lisi"; System.out.println("q.name: "+ q.name); } } class Preson{ public String name; }
輸出結果
q.name: lisi p.name: lisi
在將p
傳入printValue
方法後,方法調用結束,p
的name
屬性居然被改變了!因此得出結論,參數爲基本類型爲值傳遞,參數爲引用類型爲時爲引用傳遞。這個結論是錯誤的,下面來看看判斷是值傳遞仍是值傳遞的關鍵是什麼,先看定義
- 值傳遞:是指在調用函數時將實際參數複製一份傳遞到函數中,這樣在函數中若是對參數進行修改,將不會影響到實際參數。
- 引用傳遞:是指在調用函數時將實際參數的地址直接傳遞到函數中,那麼在函數中對參數所進行的修改,將影響到實際參數。
從定義中能夠明顯看出,區分是值傳遞仍是引用傳遞主要是看向方法中傳遞的是實際參數的副本仍是實際參數的地址。上面第一個例子很明顯是值傳遞,其實第二個例子中向printValue
方法中傳遞的是一個引用的副本,只是這個副本引用和原始的引用指向的同一個對象,因此副本引用修改過對象屬性後,經過原始引用查看對象屬性確定也是被修改過的。換句話說,printValue
方法中修改的是副本引用指向的對象的屬性,不是引用自己,若是修改的是引用自己,那麼原始引用確定不受影響。看下面這個例子
public class Main{ public static void main(String[] args) { Preson p = new Preson(); p.name = "zhangsan"; printValue(p); System.out.println("p.name: " + p.name); } public static void printValue(Preson q){ q = new Preson(); q.name = "lisi"; System.out.println("q.name: "+ q.name); } } class Preson{ public String name; }
輸出結果
q.name: lisi p.name: zhangsan
能夠看到將p
傳入printValue
方法後,printValue
方法調用結束後,p
的屬性name
沒有改變,這是由於在printValue
方法中並無改變副本引用q
所指向的對象,而是改變了副本引用q
自己,將副本引用q
指向了另外一個對象並對這個對象的屬性進行修改,因此原始引用p
所指向的對象不受影響。因此證實Java中只存在值傳遞。
IO流 *
Java IO流主要能夠分爲輸入流和輸出流。按照照操做單元劃分,能夠劃分爲字節流和字符流。按照流的角色劃分爲節點流和處理流。
Java I0流的40多個類都是從4個抽象類基類中派生出來的。
- InputStream:字節輸入流
- Reader:字符輸入流
- OutputStream:字節輸出流
- Writer:字符輸出流
BIO,NIO,AIO 有什麼區別? **
-
**BIO (Blocking I/O):**服務器實現模式爲一個鏈接一個線程,即客戶端有鏈接請求時服務器就須要啓動一個線程進行處理,若是這個鏈接不作任何事情會形成沒必要要的線程開銷,能夠經過線程池機制來改善。BIO方式適用於鏈接數目比較小且固定的架構,這種方式對服務端資源要求比較高,併發侷限於應用中,在jdk1.4之前是惟一的io
-
**NIO (New I/O):**服務器實現模式爲一個請求一個線程,即客戶端發送的鏈接請求都會註冊到多路複用器上,多路複用器輪詢到鏈接有IO請求時才啓動一個線程進行處理。NIO方式適用於鏈接數目多且鏈接比較短(輕操做)的架構,好比聊天服務器,併發侷限於應用中,編程比較複雜,jdk1,4開始支持
-
**AIO (Asynchronous I/O):**服務器實現模式爲一個有效請求一個線程,客戶端的IO請求都是由操做系統先完成了再通知服務器用其啓動線程進行處理。AIO方式適用於鏈接數目多且鏈接比較長(重操做)的架構,好比相冊服務器,充分調用OS參與併發操做,編程比較複雜,jdk1.7開始支持。
這些概念看着比較枯燥,能夠從這個經典的燒開水的例子去理解
**BIO **:來到廚房,開始燒水NIO,並坐在水壺面前一直等着水燒開。
NIO:來到廚房,開AIO始燒水,可是咱們不一直坐在水壺前面等,而是作些其餘事,而後每隔幾分鐘到廚房看一下水有沒有燒開。
AIO:來到廚房,開始燒水,咱們不一直坐在水壺前面等,而是在水壺上面裝個開關,水燒開以後它會通知我。
反射 ***
JAVA反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。
Java獲取Class對象的三種方式
class Person { public String name = "zhangsan"; public Person() { } }
public class Main{ public static void main(String[] args) throws ClassNotFoundException { //方式1 Person p1 = new Person(); Class c1 = p1.getClass(); //方式2 Class c2 = Person.class; //方式3可能會拋出ClassNotFoundException異常 Class c3 = Class.forName("com.company"); } }
由於在一個類在 JVM 中只會有一個 Class
實例,因此對c1
、c2
、c3
進行equals
比較時返回的都是true
。
反射優缺點:
- 優勢:運行期類型的判斷,動態加載類,提升代碼靈活度。
- 缺點:性能比直接的java代碼要慢不少。
反射應用場景:
- Java的不少框架都用到了反射,例如
Spring
中的xml的配置模式等 - 動態代理設計模式也採用了反射機制
JAVA異常 ***
異常主要分爲Error
和Exception
兩種
- Error:
Error
類以及他的子類的實例,表明了JVM自己的錯誤。錯誤不能被程序員經過代碼處理。 - EXception:
Exception
以及他的子類,表明程序運行時發送的各類不指望發生的事件。能夠被Java異常處理機制使用,是異常處理的核心。
異常框圖
除了以上的分類,異常還能分爲非檢查異常和檢查異常
- 非檢查異常(unckecked exception):**該類異常包括運行時異常(RuntimeException極其子類)和錯誤(Error)。**編譯器不會進行檢查而且不要求必須處理的異常,也就說當程序中出現此類異常時,即便咱們沒有
try-catch
捕獲它,也沒有使用throws
拋出該異常,編譯也會正常經過。由於這樣的異常發生的緣由極可能是代碼寫的有問題。 - 檢查異常(checked exception):**除了
Error
和RuntimeException
的其它異常。**這是編譯器要求必須處理的異常。這樣的異常通常是由程序的運行環境致使的。由於程序可能被運行在各類未知的環境下,而程序員沒法干預用戶如何使用他編寫的程序,因此必須處理這些異常。
下面來看下
try{}catch(){}finally{}
和return
之間的「恩恩怨怨」,這裏有些亂,面試時問的也不是不少,實在記不住就算啦。仍是先看代碼猜結果。
public class Main{ public static void main(String[] args) { int a = test1(); System.out.println(a); int b = test2(); System.out.println(b); int c = test3(); System.out.println(c); int d = test4(); System.out.println(d); int e = test5(); System.out.println(e); } public static int test1(){ int a = 1; try{ a = 2; return a; }catch(Exception e){ System.out.println("hello,test1"); a = 3; }finally{ a = 4; } return a; } //輸出 2 public static int test2(){ int a = 1; try{ a = 2; return a; }catch(Exception e){ System.out.println("hello,test2"); a = 3; return a; }finally{ a = 4; } } //輸出 2 public static int test3(){ int a = 1; try{ a = 2/0; return a; }catch(Exception e){ System.out.println("hello,test3"); a = 3; }finally{ a = 4; } return a; } //輸出 hello,test3 // 4 public static int test4(){ int a = 1; try{ a = 2/0; return a; }catch(Exception e){ System.out.println("hello,test4"); a = 3; return a; }finally{ a = 4; } } //輸出 hello,test4 // 3 public static int test5(){ int a = 1; try{ a = 2/0; return a; }catch(Exception e){ a = 3; return a; }finally{ a = 4; return a; } } //輸出 4 }
若是沒有仔細的研究過,應該好多會猜錯,下面總結下規律。
- 從前三個例子能夠看出若是
try{}
中的代碼沒有異常,catch(){}
代碼塊中的代碼不會執行。因此若是try{}
和catch(){}
都含有return
時,無異常執行try{}
中的return
,存在異常執行catch(){}
的return
。 - 無論任何狀況,就算
try{}
或catch(){}
中含有return
,finally{}
中的代碼必定會執行,那麼爲何test1
、test2
、test3
中的結果不是4呢,由於雖然finally
是在return
後面的表達式運算以後執行的,但此時並無返回運算以後的值,而是把值保存起來,無論finally
對該值作任何的改變,返回的值都不會改變,依然返回保存起來的值。也就是說方法的返回值是在finally
運算以前就肯定了的。 - 若是
return
的數據是引用數據類型,而在finally
中對該引用數據類型的屬性值的改變起做用,try
中的return
語句返回的就是在finally
中改變後的該屬性的值。這個不理解能夠看看上面提到的Java的值傳遞的問題。 - 若是
finally{}
中含有return
,會致使程序提早退出,不在執行try{}
或catch(){}
中的return
。因此test5
返回的值是4。
最後總計一下try{}catch(){}finally{}
的執行順序。
- 先執行
try
中的語句,包括return
後面的表達式; - 有異常時,執行
catch
中的語句,包括return
後面的表達式,無異常跳過catch
語句; - 而後執行
finally
中的語句,若是finally
裏面有return
語句,執行return
語句,程序結束; finally{}
中沒有return
時,無異常執行try
中的return
,若是有異常時則執行catch
中的return
。前兩步執行的return
只是肯定返回的值,程序並未結束,finally{}
執行以後,最後將前兩步肯定的return
的返回值返回。
JAVA註解 **
面試問的很少,可是在使用框架開發時會常用,但東西太多了,這裏只是簡單介紹下概念。
Annotation
註解能夠當作是java中的一種標記記號,用來給java中的類,成員,方法,參數等任何程序元素添加一些額外的說明信息,同時不改變程序語義。註解能夠分爲三類:基本註解,元註解,自定義註解
-
標準註解
- @Deprecated:該註解用來講明程序中的某個元素(類,方法,成員變量等)已經再也不使用,若是使用的話的編譯器會給出警告。
- @SuppressWarnings(value=「」):用來抑制各類可能出現的警告。
- @Override:用來講明子類方法覆蓋了父類的方法,保護覆蓋方法的正確使用
-
元註解(元註解也稱爲元數據註解,是對註解進行標註的註解,元註解更像是一種對註解的規範說明,用來對定義的註解進行行爲的限定。例如說明註解的生存週期,註解的做用範圍等)
- @Target(value=「 」):該註解是用來限制註解的使用範圍的,即該註解能夠用於哪些程序元素。
- @Retention(value=「 」):用於說明註解的生存週期
- @Documnent:用來講明指定被修飾的註解能夠被javadoc.exe工具提取進入文檔中,全部使用了該註解進行標註的類在生成API文檔時都在包含該註解的說明。
- @Inherited:用來講明使用了該註解的父類,其子類會自動繼承該註解。
- @Repeatable:java1.8新出的元註解,若是須要在給程序元素使用相同類型的註解,則需將該註解標註上。
-
自定義註解:用@Interface來聲明註解。
JAVA泛型 ***
Java 泛型是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制容許程序員在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說所操做的數據類型被指定爲一個參數。
-
泛型擦除(這是面試考察泛型時常常問到的問題)
Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成爲類型擦除。看下面代碼
public class Main{ public static void main(String[] args) { ArrayList<Integer> arrayList1 = new ArrayList<>(); ArrayList<String> arrayList2 = new ArrayList<>(); System.out.println(arrayList1.getClass() == arrayList2.getClass()); } }
輸出結果
true
能夠看到
ArrayList<Integer>
和ArrayList<String>
的原始類型是相同,在編譯成字節碼文件後都會變成List
,JVM看到的只有List
,看不到泛型信息,這就是泛型的類型擦除。在看下面這段代碼public class Main{ public static void main(String[] args) throws Exception { ArrayList<Integer> arrayList = new ArrayList<>(); arrayList.add(1); arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "a"); System.out.println(arrayList.get(0)); System.out.println(arrayList.get(1)); } }
輸出
1 a
能夠看到經過反射進行
add
操做,ArrayList<Integer>
居然能夠存儲字符串,這是由於在反射就是在運行期調用的add
方法,在運行期泛型信息已經被擦除。 -
既然存在類型擦除,那麼Java是如何保證在
ArrayList<Integer>
添加字符串會報錯呢?Java編譯器是經過先檢查代碼中泛型的類型,而後在進行類型擦除,再進行編譯。
JAVA序列化 **
-
序列化:將對象寫入到IO流中
-
反序列化:從IO流中恢復對象
-
序列化的意義:將Java對象轉換成字節序列,這些字節序列更加便於經過網絡傳輸或存儲在磁盤上,在須要時能夠經過反序列化恢復成原來的對象。
-
實現方式:
- 實現Serializable接口
- 實現Externalizable接口
-
序列化的注意事項:
- 對象的類名、實例變量會被序列化;方法、類變量、
transient
實例變量都不會被序列化。 - 某個變量不被序列化,可使用
transient
修飾。 - 序列化對象的引用類型成員變量,也必須是可序列化的,不然,會報錯。
- 反序列化時必須有序列化對象的
class
文件。
- 對象的類名、實例變量會被序列化;方法、類變量、
深拷貝與淺拷貝 ***
-
深拷貝:對基本數據類型進行值傳遞,對引用數據類型,建立一個新的對象,並複製其內容,兩個引用指向兩個對象,但對象內容相同。
-
淺拷貝:對基本數據類型進行值傳遞,對引用數據類型複製一個引用指向原始引用的對象,就是複製的引用和原始引用指向同一個對象。
具體區別看下圖
-
深拷貝的實現方式
-
重載
clone
方法public class Main{ public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, CloneNotSupportedException { Address s = new Address("天津"); Person p = new Person("張三", 23, s); System.out.println("克隆前的地址:" + p.getAddress().getName()); Person cloneP = (Person) p.clone(); cloneP.getAddress().setName("北京"); System.out.println("克隆後的地址:" + cloneP.getAddress().getName()); } } class Address implements Cloneable { private String city; public Address(String name){ this.city = name; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String getName() { return city; } public void setName(String name) { this.city = name; } } class Person implements Cloneable{ private String name; private int age; private Address address; public Person(String name, int age, Address address){ this.name = name; this.age = age; this.address = address; } @Override public Object clone() throws CloneNotSupportedException { Person person = (Person) super.clone(); person.address = (Address)address.clone(); return person; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } }
輸出
克隆前的地址:天津 克隆後的地址:北京
其實就是
Person
類和Address
類都要重寫clone
方法,這裏面須要注意的一點是super.clone()
爲淺克隆,因此在在Person
類中重寫clone
方法時,address
對象須要調用address.clone()
從新賦值,由於address
類型爲引用類型。 -
序列化
public class Main{ public static void main(String[] args) throws IOException, ClassNotFoundException { Address s = new Address("天津"); Person p = new Person("張三", 23, s); System.out.println("克隆前的地址:" + p.getAddress().getName()); Person cloneP = (Person) p.deepClone(); cloneP.getAddress().setName("北京"); System.out.println("克隆後的地址:" + cloneP.getAddress().getName()); } } class Address implements Serializable{ private String city; public Address(String name){ this.city = name; } public String getName() { return city; } public void setName(String name) { this.city = name; } } class Person implements Serializable{ private String name; private int age; private Address address; public Person(String name, int age, Address address){ this.name = name; this.age = age; this.address = address; } public Object deepClone() throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } }
輸出
克隆前的地址:天津 克隆後的地址:北京
-
常見的Object方法 ***
這些方法都很重要,面試常常會問到,要結合其餘知識將這些方法理解透徹
Object clone()
:建立與該對象的類相同的新對象boolean equals(Object)
:比較兩對象是否相等void finalize()
:當垃圾回收器肯定不存在對該對象的更多引用時,對象垃圾回收器調用該方法Class getClass()
:返回一個對象運行時的實例類int hashCode()
:返回該對象的散列碼值void notify()
:喚醒等待在該對象的監視器上的一個線程void notifyAll()
:喚醒等待在該對象的監視器上的所有線程String toString()
:返回該對象的字符串表示void wait()
:在其餘線程調用此對象的notify()
方法或notifyAll()
方法前,致使當前線程等待