面向對象
java三大特性
- 封裝: 將事務封裝成一個類,達到解耦,隱藏細節的效果。通過get/set等方法,封裝了內部邏輯,並保留了特定的接口與外界聯系。
- 繼承: 從一個已知的類中派生出一個新的類,新類可以擁有已知類的行為和屬性,並且可以通過覆蓋/重寫來增強已知類的能力。
- 多態: 同一個實現接口,使用不同的實例而執行不同的操作。繼承是多態的基礎,沒有繼承就沒有多態。
關於繼承
- Java中不支持多繼承,即一個類只可以有一個父類存在。
- Java中的構造函數是不可以繼承的,如果一個父類構造函數時私有的,那么久不允許有子類存在。
- 子類擁有父類非private的屬性和方法
- 子類可以添加自己的方法和屬性,即對父類進行擴展
- 子類可以重新定義父類的方法,即方法的覆蓋/重寫
關於覆蓋/重寫(@Override)
子類中的方法與父類中繼承的方法有完全相同的返回值類型、方法名、參數個數以及參數類型,但子類豐富或修改了功能。
關於重載
重載是指在一個類中(包括父類)存在多個同名的不同方法,這些方法的參數個數,順序以及類型不同均可以構成方法的重載。如果僅僅是修飾符、返回值、拋出的異常不同,那么這是兩個相同的方法。
所以只有方法返回值不同是不能構成重載的
如:
public int add(int a, int b){}
public void add(int a, int b){}
在調用add方法的時候,系統並不知道你要調用哪一個。
子類與父類的相互轉型
假設有兩個類,Father是父類,Son是其子類。
- 向上轉型
子類對象轉為父類,父類可以是接口。
Father f1 = new Son();
問題:在向上轉型的過程中,我們容易出現方法丟失的問題,比如我們將一個導出類(子類)進行向上轉型,但是在基類(父類)中可能缺少部分導出類(子類)的方法,所以我們對導出類(子類)進行向上轉型之后,使用基類(父類)對象去調用方法,只能調用基類(父類)有的方法。
- 向下轉型
父類對象轉為子類
Son s1 = (Son) f1;
注意:該父類必須實際指向了一個子類對象才可強制類型向下轉型,看看下面的錯誤例子
Father f2 = new Father();
Son s2 = (Son)f2;
為什么Son s1 = (Son) f1;
可以轉型而Son s2 = (Son)f2;
轉型失敗?
因為 f1 指向一個子類對象,但 f2 被傳給了一個 Father 對象,這也就說明了該父類必須實際指向了一個子類對象才可強制類型向下轉型
JDK, JRE, JVM
- JDK: java 開發工具包,包含了JRE,同時還包含了java編譯器javac、監控工具jconsole、分析工具jvisualvm
- JRE: java 運行時環境,包含了java虛擬機、java基礎類庫
- JVM: 是指Java虛擬機,當我們運行一個程序時,JVM負責將字節碼轉換為特定機器代碼,JVM提供了內存管理/垃圾回收和安全機制等
區別與聯系:
- JDK是開發工具包,用來開發Java程序,而JRE是Java的運行時環境
- JDK和JRE中都包含了JVM
- JVM是Java編程的核心,獨立於硬件和操作系統,具有平台無關性,而這也是Java程序可以一次編寫,多處執行的原因
Java語言的平台無關性是如何實現的?
- JVM屏蔽了操作系統和底層硬件的差異
- Java面向JVM編程,先編譯生成字節碼文件,然后交給JVM解釋成機器碼執行
- 通過規定基本數據類型的取值范圍和行為
抽象類與接口
java中通過abstract
來定義抽象類,通過interface
關鍵字來定義接口
抽象類和接口的主要區別可以總結如下:
- 抽象類中可以沒有抽象方法,也可以抽象方法和非抽象方法共存
- 接口中的方法在JDK8之前只能是抽象的,JDK8版本開始提供了接口中方法的default實現
- 抽象類和類一樣是單繼承的,但接口可以繼承多個接口
- 抽象類中可以存在普通的成員變量;接口中的變量必須是static final類型的,必須被初始化,接口中只有常量,沒有變量
8種基本數據類型
數據類型 | 字節 | 位 |
---|---|---|
byte | 1 | 8 |
short | 2 | 16 |
int | 4 | 32 |
long | 8 | 64 |
float | 4 | 32 |
double | 8 | 64 |
char | 2 | 16 |
boolean | - | - |
注:boolean沒有規定
==、equals、hashcode
背景介紹
- == 比較的是變量(棧)內存中存放的對象的(堆)內存地址,用來判斷兩個對象的地址是否相同,即是否是指相同一個對象。比較的是真正意義上的指針操作。
- equals用來比較的是兩個對象的內容是否相等,由於所有的類都是繼承自java.lang.Object類的,所以適用於所有對象,如果沒有對該方法進行覆蓋的話,調用的仍然是Object類中的方法,而Object中的equals方法返回的卻是==的判斷。
- hashCode() 方法用於返回字符串的哈希碼,如果兩個對象相同,那么它們的hashCode值一定要相同。
總結:
- 對於復合數據類型之間進行equals比較,在沒有覆寫equals方法的情況下,他們之間的比較還是內存中的存放位置的地址值,跟雙等號(==)的結果相同;如果被復寫,按照復寫的要求來。
- 對於 ==
- 是個運算符
- 基本類型:比較的就是值是否相同
- 引用類型:比較的就是地址值是否相同
- 對於equals()
- 是個方法
- 基本類型:與==一致,都是比較值
- 引用類型:默認情況下,比較的是地址值,重寫該方法后比較對象的成員變量值是否相同,但在一些類庫中已經重寫了這個方法(一般都是用來比較對象的成員變量值是否相同),比如:String,Integer,Date 等類中,所以他們不再是比較類在堆中的地址了。
- 對於hashcode()
- 是個方法,返回值是一個對象的哈希碼
- 如果兩個對象通過equals方法比較相等,那么他的hashCode一定相等;如果兩個對象通過equals方法比較不相等,那么他的hashCode有可能相等;
- 重寫equals方法,一定要重寫hashCode方法
靜態與非靜態
- 靜態變量:由static修飾,在JVM中,靜態變量的加載順序在對象之前,因此靜態變量不依附於對象存在,可以在不實例化類的情況下直接使用靜態變量
- 實例變量(非靜態):必須依附於對象存在,只有實例化類后才可以使用此類中的實例變量。
static關鍵字
這個問題更多的可以引向static關鍵字來解答,從static中我們就能看出靜態與非靜態大致的區別
- static 修飾表示靜態或全局,被修飾的屬性和方法屬於類,可以用類名.靜態屬性 / 方法名 訪問
- static 修飾的代碼塊表示靜態代碼塊,當 Java 虛擬機(JVM)加載類時,就會執行該代碼塊,只會被執行一次
- static 修飾的屬性,也就是類變量,是在類加載時被創建並進行初始化,只會被創建一次
- static 修飾的變量可以重新賦值
- static 方法中不能用 this 和 super 關鍵字
- static 方法必須被實現,而不能是抽象的abstract
- static 方法不能被重寫
注解
注解的本質就是一個繼承了 Annotation 接口的接口,用來將任何的信息或元數據(metadata)與程序元素(類、方法、成員變量等)進行關聯。程序可以通過反射獲取標注內容。
注解的作用
代替繁雜的配置文件,簡化開發。
定義注解
public @interface MyAnnotation {
String value();
int value1();
}
// 使用注解MyAnnotation,可以設置屬性
@MyAnnotation(value1=100,value="hello")
public class MyClass {
}
從代碼可以看出,定義注解使用的是@interface,可以在方法內部定義屬性。
元注解
元注解的作用就是負責注解其他注解。Java中提供了4個元注解。
- @Target:說明注解所修飾的對象范圍
源碼如下:
public @interface Target {
ElementType[] value();
}
public enum ElementType {
TYPE,FIELD,METHOD,PARAMETED,CONSTRUCTOR,LOCAL_VARIABLE,ANNOCATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE
}
用例
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAnnotation {
}
說明: 這表明MyAnnotation只能作用域類/接口和方法上。
- @Retention:(保留策略): 保留策略定義了該注解被保留的時間長短。
源碼如下:
public @interface Retention {
RetentionPolicy value();
}
public enum RetentionPolicy {
SOURCE, CLASS, RUNTIME
}
說明: SOURCE:表示在源文件中有效(即源文件保留);CLASS:表示在class文件中有效(即class保留);RUNTIME:表示在運行時有效(即運行時保留)
- @Documented: 聲明注解能夠被javadoc等識別
- @Inherited: 聲明子類可以繼承此注解,如果一個類A使用此注解,則類A的子類也繼承此注解
反射
反射機制是指在運行中,對於任意一個類,都能夠知道這個類的所有屬性和方法。對於任意一個對象,都能夠調用它的任意一個方法和屬性。即動態獲取信息和動態調用對象方法的功能稱為反射機制。
Java反射機制提供的功能
- 在運行時判斷任意一個對象所屬的類
- 在運行時構造任意一個類的對象
- 在運行時判斷或調用任意一個類所具有的成員變量和方法
- 在運行時獲取泛型信息
- 在運行時處理注解
- 生成動態代理
Java反射優點和缺點
優點:可以實現動態創建對象和編譯,體現出很大的靈活性
缺點:對性能有影響,使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什么並且它滿足我們的要求。這類操作總是慢於直接執行相同的操作。
反射相關的主要API
- java.lang.Class:代表一個類
- java.lang.reflect.Method :代表類的方法
- java.lang.reflect.Field :代表類的成員變量
- java.lang.reflect.Constructor :代表類的構造器
如下例子:
package com.reflection;
public class Demo01 {
public static void main(String[] args) throws ClassNotFoundException {
/*通過反射獲取類的Class對象,
*一個類只有一個Class對象,所以c1,c2,c3的hashcode相同
*一個類被加載后,整個類的結構都會被封裝在Class對象中
*/
Class c1 = Class.forName("com.reflection.User");
System.out.println(c1.getName());//com.reflection.User
Class c2 = Class.forName("com.reflection.User");
System.out.println(c2.hashCode());
Class c3 = Class.forName("com.reflection.User");
System.out.println(c3.hashCode());
}
}
//實體類:pojo,entity,只有屬性
class User{
private String name;
private int age;
public User(){
}
public User(String name,int age){
this.age = age;
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
得到的結果入下:
com.reflection.User
356573597
356573597
說明: 通過反射獲取類的Class對象,一個類只有一個Class對象,所以c1,c2,c3的hashcode相同,一個類被加載后,整個類的結構都會被封裝在Class對象中。
獲取Class類的實例
- 若已知具體的類, 通過類的class屬性獲取, 該訪法最為安全可靠,程序性能最高。
Class class = Person.class;
- 已知某個類的實例, 調用該實例的getClass()方法獲取Class對象.
Class class = person.getClass();
- 已知一 個類的全類名,且該類在類路徑下,可通過Class類的靜態方法forName()獲取.
Class class = Class forName("demo01. Student");
- 內置基本數據類型可以直接用類名.Type
Class c4 = Integer.TYPE;
調用指定的方法
通過反射,調用類中的方法,通過Method類完成。
- 通過Class類的getMethod(String name,Clas…parameterTypes)方法取得一個Method對象,並設置此方法操作時所需要的參數類型。
- 之后使用Object invoke(Object obj, Object[] args)進行調用,並向方法中傳遞要設置的obj對象的參數信息。
java異常體系
異常主要分為Exception和Error
Throwable 是 Java 語言中所有錯誤與異常的超類。Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生了異常情況。
Exception和Error有什么區別?
- Exception是程序正常運行中預料到可能會出現的錯誤,並且應該被捕獲並進行相應的處理,是一種異常現象.Exception又分為了運行時異常和編譯時異常。
- 編譯時異常(受檢異常): 表示當前調用的方法體內部拋出了一個異常,所以編譯器檢測到這段代碼在運行時可能會出異常,所以要求我們必須對異常進行相應的處理,可以捕獲異常或者拋給上層調用方。
- 運行時異常(非受檢異常): 表示在運行時出現的異常,常見的運行時異常包括:空指針異常,數組越界異常,數字轉換異常以及算術異常等。
- Error是正常情況下不可能發生的錯誤,Error會導致JVM處於一種不可恢復的狀態
NoClassDefFoundError 和 ClassNotFoundException 區別?
- NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在內存中找不到該類的定義,該動作發生在運行期間,即編譯時該類存在,但是在運行時卻找不到了,可能是變異后被刪除了等原因導致;
- ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,或在方法簽名中用 throws 關鍵字進行聲明。當使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動態加載類到內存的時候,通過傳入的類路徑參數沒有找到該類,就會拋出該異常;另一種拋出該異常的可能原因是某個類已經由一個類加載器加載至內存中,另一個加載器又嘗試去加載它。
捕獲異常應該遵循哪些原則
- 不要使用Exception捕獲,而是盡可能詳細的寫出要捕獲的異常;
- 使用日志記錄,方便以后排查;
- 不要使用try-catch去包住一塊很大的代碼塊,這不利於排查問題;
java異常處理關鍵字
throw 和 throws 的區別是什么?
- throw 關鍵字用在方法內部,只能用於拋出一種異常,用來拋出方法或代碼塊中的異常,受查異常和非受查異常都可以被拋出。
- throws 關鍵字用在方法聲明上,可以拋出多個異常,用來標識該方法可能拋出的異常列表。一個方法用 throws 標識了可能拋出的異常列表,調用該方法的方法中必須包含可處理異常的代碼,否則也要在方法簽名中用 throws 關鍵字聲明相應的異常。
Java中的值傳遞和引用傳遞
- 值傳遞,意味着傳遞了對象的一個副本,即使副本被改變,也不會影響源對象。
- 引用傳遞,意味着傳遞的並不是實際的對象,而是對象的引用。因此,外部對引用對象的改變會反映到所有的對象上。
值傳遞的例子
public class Test {
public static void main(String[] args) {
int x=0;
change(x);
System.out.println(x);
}
static void change(int i){
i=7;
}
}
眾所周知,結果是0。
因為如果參數是基本數據類型,那么是屬於值傳遞的范疇,傳遞的其實是源對象的一個copy副本,不會影響源對象的值。
再來看看一個引用傳遞的例子
public class Test {
public static void main(String[] args) {
StringBuffer x = new StringBuffer("Hello");
change(x);
System.out.println(x);
}
static void change(StringBuffer i) {
i.append(" world!");
}
}
輸出為Hello world!
為什么不是輸出Hello了?
- 在①中,x指向的是堆內存,存放hello
- 在②中,i也指向了堆內存的hello
- 在③中,x和i指向了同樣的內存地址,所以append操作將直接修改了內存地址里邊的值
- 在④中,方法結束,局部變量i消失
再看一個例子
public class Test {
public static void main(String[] args) {
StringBuffer x = new StringBuffer("Hello");
change2(x);
System.out.println(x);
}
static void change2(StringBuffer i) {
i = new StringBuffer("hi");
i.append(" world!");
}
}
輸出是Hello,我們同樣通過圖來分析
如上圖所示,在函數change2中將引用變量i重新指向了堆內存中另一塊區域,下邊都是對另一塊區域進行修改,所以輸出是Hello。
最后再看一個例子
public class Test {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("Hello ");
System.out.println("Before change, sb = " + sb);
changeData(sb);
System.out.println("After change, sb = " + sb);
}
public static void changeData(StringBuffer strBuf) {
StringBuffer sb2 = new StringBuffer("Hi,I am ");
strBuf = sb2;
sb2.append("World!");
}
}
兩者都是輸出sb = Hello
before change我相信大家都知道為什么。
after change中,首先strBuf是指向堆內存中的Hello的,但是由於sb2指向了堆內存中的Hi I am,之后strBuf = sb2
,導致strBuf指向了堆內存中的Hi I am,這對sb指向的堆內存沒有什么改變,因此最后輸出sb依然是Hello。
String、StringBuffer與StringBuilder
三者運行速度不同
String(遍歷一百萬次)
String aa = "";
long startTime = System.currentTimeMillis();
for(int i=0;i<100*100*10;i++){
//字符串拼接
aa = aa + "aa";
}
long endTime = System.currentTimeMillis();
System.out.println("耗時:"+String.valueOf(endTime - startTime));
運行結果:
耗時:7614
StringBuffer(遍歷一億次)
StringBuffer aa = new StringBuffer();
String ss = "ss";
long startTime = System.currentTimeMillis();
for(int i=0;i<100*100*100*100;i++){
//字符串拼接
aa.append(ss);
}
long endTime = System.currentTimeMillis();
System.out.println("耗時:"+String.valueOf(endTime - startTime));
運行結果:
耗時:3128
StringBuilder(遍歷一億次)
StringBuilder aa = new StringBuilder();
String ss = "ss";
long startTime = System.currentTimeMillis();
for(int i=0;i<100*100*100*100;i++){
//字符串拼接
aa.append(ss);
}
long endTime = System.currentTimeMillis();
System.out.println("耗時:"+String.valueOf(endTime - startTime));
運行結果:
耗時:1240
速度比較:String < StringBuffer < StringBuilder,且String的處理速度比StringBuffer、StringBuilder要慢的多
分析String的處理速度為什么要比StringBuffer、StringBuilder慢的多?
- String是不可變的對象
- StringBuffer和StringBuilder是可變對象
(1)String本身就是一個對象,因為String不可變對象,所以,每次遍歷對字符串做拼接操作,都會重新創建一個對象,循環100萬次就是創建100萬個對象,非常的消耗內存空間,而且創建對象本身就是一個耗時操作,創建100萬次對象就相當的耗時了。
(2)StringBuffer和StringBuilder只需要創建一個StringBuffer或StringBuilder對象,然后用append拼接字符串,就算拼接一億次,仍然只有一個對象。
是不是可以拋棄使用String,轉而使用StringBuffer和StringBuilder呢?
不行!
(1)String遍歷代碼:一開始定義一個String常量(創建一個String對象), 再開始遍歷;
(2)StringBuffer代碼:一開始定義一個String常量(創建一個String對象)和一個創建StringBuffer對象,再開始遍歷;
(3)StringBuiler代碼:一開始定義一個String常量(創建一個String對象)和一個創建StringBuiler對象,再開始遍歷;
(2)和(3)比(1)多了一個創建對象流程,所以,如果數據量比較小的情況建議使用String。
說說StringBuffer和StringBuilder的區別?
- StringBuffer是線程安全的
- StringBuilder是非線程安全的, 這也是速度比StringBuffer快的原因
使用場景?
(1)如果要操作少量的數據用 String
(2)單線程操作字符串緩沖區 下操作大量數據 StringBuilder
(3)多線程操作字符串緩沖區 下操作大量數據 StringBuffer
泛型
泛型就是參數化類型,在不創建新的數據類型情況下,通過泛型控制具體不同類型的形參。泛型最常用的場景就是在集合中,能夠簡化開發,並且能夠保證代碼質量。泛型是在編譯期間有效,在運行階段就會去泛型化,也就是將泛型信息抹掉,這也是不支持泛型數組的原因。
- 通過泛型的語法定義,編譯器可以在編譯期提供一定的類型安全檢查,過濾掉大部分因為類型不符而導致的運行時異常。
- 泛型可以讓程序代碼的可讀性更高,並且由於本身只是一個語法糖,所以對於 JVM 運行時的性能是沒有任何影響的。
序列化與反序列化
序列化中的關鍵字transient
,被他修飾的變量在序列化對象的過程中不會被序列化。
序列化的意思就是將對象的狀態轉化成字節流,以后可以通過這些值再生成相同狀態的對象。對象序列化是對象持久化的一種實現方法,它是將對象的屬性和方法轉化為一種序列化的形式用於存儲和傳輸。反序列化就是根據這些保存的信息重建對象的過程。
序列化:將java對象轉化為字節序列的過程。
反序列化:將字節序列轉化為java對象的過程。
優點:
a、實現了數據的持久化,通過序列化可以把數據永久地保存到硬盤上(通常存放在文件里)
b、利用序列化實現遠程通信,即在網絡上傳送對象的字節序列。
反序列化失敗的場景:
序列化ID:serialVersionUID不一致的時候,導致反序列化失敗
Java 對象在 JVM 退出時會全部銷毀,如果需要將對象持久化就要通過序列化實現,將內存中的對象保存在二進制流中,需要時再將二進制流反序列化為對象。對象序列化保存的是對象的狀態,屬於類屬性的靜態變量不會被序列化。
常見的序列化有三種:① Java 原生序列化,實現 Serializabale 標記接口,兼容性最好,但不支持跨語言,性能一般。序列化和反序列化必須保持序列化 ID 的一致,一般使用 private static final long serialVersionUID 定義序列化 ID,如果不設置編譯器會根據類的內部實現自動生成該值。② Hessian 序列化,支持動態類型、跨語言。③ JSON 序列化,將數據對象轉換為 JSON 字符串,拋棄了類型信息,反序列化時只有提供類型信息才能准確進行。相比前兩種方式可讀性更好。
序列化通常使用網絡傳輸對象,容易遭受攻擊,因此不需要進行序列化的敏感屬性應加上 transient
關鍵字,把變量生命周期僅限於內存,不會寫到磁盤。
詳細解析請看:
java基礎---->Serializable的使用