JAVA面試 基礎加強與鞏固:反射、註解、泛型等

小楠總發表於2017-12-21

作者-煥然一璐,支援原創,轉載請註明出處,謝謝合作。 原文連結:http://www.jianshu.com/p/aaf8594e02eb

企業重視的是學習能力:基礎很重要

###JDK1.5新特性

  1. 泛型
  2. foreach
  3. 自動拆箱裝箱
  4. 列舉
  5. 靜態匯入(Static import)
  6. 後設資料(Metadata)
  7. 執行緒池
  8. 註解

###JDK1.6新特性

  1. Desktop類和SystemTray類
  2. 使用JAXB2來實現物件與XML之間的對映
  3. StAX
  4. 使用Compiler API
  5. 輕量級Http Server API
  6. 插入式註解處理API(Pluggable Annotation Processing API)
  7. 用Console開發控制檯程式
  8. 對指令碼語言的支援
  9. Common Annotations

###JDK1.7新特性

1 對集合類的語言支援; 2 自動資源管理; 3 改進的通用例項建立型別推斷; 4 數字字面量下劃線支援; 5 switch中使用string; 6 二進位制字面量; 7 簡化可變引數方法呼叫。

###JDK1.8新特性

  1. 介面的預設方法,也就是介面中可以有實現方法
  2. 函式式介面與靜態匯入
  3. Lambda 表示式
  4. 訪問區域性變數

靜態匯入:JDK1.5新特性

//靜態匯入某一個方法
import static java.lang.Math.abs;
//靜態匯入一個類的所有靜態方法
import static java.lang.Math.*;

public static void main(String[] args) {
	//方法可以直接使用
    System.out.println(abs(-100));
}


//如果類裡面原本就有同名的方法的話,就會覆蓋掉靜態匯入的方法了
public static void abs() {

}
複製程式碼

###可變引數

沒有可變引數之前,實現不定長引數只能通過方法的過載實現,但是這樣做工作量很大。

沒有可變引數之前,實現可變引數的方法可以通過Obiect[]來實現。

  1. 可變引數只能放在引數列表的最後一個

  2. 編譯器隱含為可變引數建立陣列,因此可以通過陣列的方式去使用可變引數

     public static int add(int x, int... args) {
         int sum = x;
    
         for (int i = 0; i < args.length; i++) {
             sum += args[i];
         }
    
         return sum;
     }
    複製程式碼

###增強for迴圈

	//陣列或者實現了Iterable介面的物件
    for 修飾符 Type arg : 要迭代的物件) {
        
    }
複製程式碼

###基本資料型別的自動裝箱與拆箱

    //自動裝箱示例,自動將基本資料型別包裝成物件
    Integer i1 = 1000;
    Integer i2 = 1000;

    //自動拆箱示例,自動將物件解包成基本資料型別
    System.out.println(i1 + 10);


    //如果數值在-128到127之前,物件會複用(享元設計模式)
    System.out.println(i1 == i2);
複製程式碼

-128到127會緩衝起來,節省記憶體。這是享元設計模式的應用,內部狀態/外部狀態(可變)

###列舉

####為什麼要使用列舉? 比如,我們要使用1-7代表星期一到星期天,那麼通常我們會想到的做法做,定義一個類,並且提供一些公有的靜態常量,例如:

public class WeekDay {
    public static final int SUN = 1;
    public static final int MON = 2;
}
複製程式碼

但是當我們使用的時候,有些人可能不想去理會這個細節,比如會直接傳入1(可能他自己覺得1是代表星期一的),因此執行的時候就會出現一些意想不到的問題。

為了解決這個問題,java 5重新引入列舉,保證變數的值只能取指定值中的某一個,通過在程式碼編譯的時候就可以知道傳的值是否合法。

####列舉的模擬:

/**
 * 自己手動實現列舉類
 * 1. 構造方法必須是私有的
 * 2. 提供公有的靜態成員變數代表列舉,並且通過匿名內部子類去生成物件
 * 3. 對外提供的方法必須是抽象的
 */
public abstract class WeekDay {

    public static WeekDay MON = new WeekDay(0) {
        @Override
        public WeekDay next() {
            return SUN;
        }

        @Override
        public String toString() {
            return "SUN";
        }

    };

    public static WeekDay SUN = new WeekDay(1) {
        @Override
        public WeekDay next() {
            return MON;
        }

        @Override
        public String toString() {
            return "MON";
        }
    };

    private int num;

    //私有構造方法
    private WeekDay() {

    }

    //在生成匿名內部類的時候,可以傳參給父類的建構函式
    private WeekDay(int num) {
        this.num = num;
    }

    //對外提供的抽象方法,由子類去實現
    public abstract WeekDay next();

}
複製程式碼

一些關鍵的點已經在上面的註釋中給出,在使用的時候,我們只能通過這樣去生成WeekDay物件。(實際上列舉內部也是生成物件嘛)

WeekDay weekDay = WeekDay.MON;
weekDay.next();
複製程式碼

列舉的實現

public enum WeekDay {

    //列舉物件必須放在最前面,匿名內部類的建立可以帶引數,必須實現父類的抽象方法
    MON(1) {
        public WeekDay next() {
            return SUN;
        }
    },

    SUN(2) {
        public WeekDay next() {
            return MON;
        }
    };

    private int num;

    //列舉的建構函式是預設為private的,可以帶引數
    WeekDay(int num) {
        this.num = num;
    }

    public abstract WeekDay next();

}
複製程式碼

列舉使用,以及一些列舉特有的方法:

//使用方法,跟一般的物件是一模一樣的
WeekDay w = WeekDay.MON;
//直接列印列舉物件實際上是呼叫了toString
System.out.println(w);
//列印列舉的名字,實際上列印類的簡短的名字w.getClass().getSimpleName()
System.out.println(w.name());
//列印列舉物件在列舉中的位置,0開始算
System.out.println(w.ordinal());
//通過字串去或者獲取(構造)列舉物件
System.out.println(WeekDay.valueOf("MON"));

//獲取列舉類的所有物件,通過陣列的方式去遍歷
for (WeekDay value : WeekDay.values()) {
    System.out.println(value);
}
複製程式碼

####列舉的特殊用法---利用列舉可以簡簡單單就實現單例模式

###反射 -- JDK1.2就有了

瞭解反射的基礎--Class類

用來描述Java類的類就是Class這個類。

每個類在java虛擬機器中佔用一片記憶體空間,裡面的內容就是對應這個類的位元組碼(Class)

####Class位元組碼的獲取:

類名.class
物件.getClass
Class.forName("類的全名");
複製程式碼

其中,跟反射密切相關的就是forName這個方法,通過類名去獲取位元組碼。前面兩種都是虛擬機器中已經載入過了。forName方法在當虛擬機器中沒有載入過對應位元組碼的時候,就會去動態地載入進來;當已經載入過的時候,直接複用載入過的。

####九種預定義好的基本Class位元組碼

八種資料型別,外加void也有對應的位元組碼。下面給出一些例子:

Class<Integer> type = Integer.TYPE;
Class<Integer> integerClass = int.class;
Class<Void> voidClass = Void.class;
複製程式碼

例子:

public static void main(String[] args) throws Exception {
    
    Class<?> c1 = Class.forName("java.lang.String");
    Class<String> c2 = String.class;
    Class<? extends String> c3 = new String("123").getClass();

	//返回的都是同一份位元組碼,因此==
    System.out.println(c1 == c2);
    System.out.println(c2 == c3);

	//判斷是不是基本型別(九種)
    System.out.println(int.class.isPrimitive());
    System.out.println(int[].class.isPrimitive());
	
	//判斷是不是陣列(陣列也是一種型別,所有型別都可以反射)
    System.out.println(int[].class.isArray());
    
}
複製程式碼

####反射的概念

一句話總結:反射就是把Java類中的各種成分通過java的反射API對映成相應的Java類,得到這些類以後就可以對其進行使用。比如方法,構造方法,成員變數,型別,包等。下面分別取講解。

Constructor類

得到所有的構造方法

Constructor<?>[] constructors = Class.forName("java.lang.String").getConstructors();
複製程式碼

得到指定引數的某一個構造方法:

Constructor<?> constructor = Class.forName("java.lang.String").getConstructor(StringBuffer.class);
複製程式碼

建立物件

String s = (String) constructor.newInstance(new StringBuffer("abc"));
System.out.println(s.charAt(2));

也可以直接呼叫無引數的構造,實際上也是先找到Constructor再呼叫Constructor的newInstance
String s = (String) Class.forName("java.lang.String").newInstance();
複製程式碼

檢視原始碼可以發現,Class的newInstance方法中有把Constructor快取起來的。因為反射的使用會大大降低系統的效能,對於計算機來說比較耗時,而且也容易發生執行時異常,因此需要慎重使用。

Field物件

Field代表類中的一個成員變數

例如我們有一個測試的類Point

public class Point {

    public int x;
    private int y;
    protected int z;

    public Point(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}
複製程式碼

那麼,我們可以利用發射機制去得到虛擬機器中某個物件的成員變數,並且獲取他們的值

Point point = new Point(1, 2, 3);

//通過反射拿到成員變數的值
Field x = point.getClass().getField("x");
System.out.println(x.get(point));

//如果是私有或者保護變數的話,不能拿到Field,會報找不到Field的錯誤
//Field y = point.getClass().getField("y");

//私有變數需要通過使用getDeclaredField去獲取,如果要獲取值的話,需要設定setAccessible為true
//這樣的反射比較暴力
Field y = point.getClass().getDeclaredField("y");
y.setAccessible(true);
System.out.println(y.get(point));
複製程式碼

例子,把物件中所有String型別的值中的a修改為*:

public class Reflecttest1 {

    public static void main(String[] args) throws Exception {
        Test test = new Test();
        changeString(test);
        System.out.println(test);
    }

    private static void changeString(Object obj) throws Exception {
        for (Field field : obj.getClass().getFields()) {
            
            //判斷是不是String型別,注意這裡最好使用==
            if (field.getType() == String.class) {
                String oldV = (String) field.get(obj);
                String newV = oldV.replace('a', '*');
                field.set(obj, newV);
            }
        }
    }
}
複製程式碼

####Method類

String a = "0123";
//獲取方法,並且呼叫
Method charAt = a.getClass().getMethod("charAt", int.class);
//通過反射呼叫方法,第一個引數是物件,如果為靜態方法的話,應該傳null
//第二個引數是可變長引數,傳入的是實參
System.out.println(charAt.invoke(a, 1));
複製程式碼

呼叫指定物件的main方法,注意其中傳遞字串陣列的問題

由於可變長引數的問題,jdk為了相容1.4以下的版本,會把傳進去的陣列進行拆包。因此註釋程式碼會報引數不匹配的錯。

解決方案是

1、再利用Object陣列包裝一層,告訴編譯器,可以拆包,但是拆開之後就是一個String陣列。

2、相強制轉換為Object物件,告訴編譯器,不要拆包。

public class ReflectTest3 {

    public static void main(String[] args) throws Exception {

        Method mainMethod = Class.forName("com.nan.test.T").getMethod("main", String[].class);
//        mainMethod.invoke(null, new String[]{"123"});
        mainMethod.invoke(null, new Object[]{new String[]{"123"}});
        mainMethod.invoke(null, (Object) new String[]{"123"});

    }

}

class T {
    public static void main(String[] args) {
        for (String s : args) {
            System.out.println(s);
        }
    }
}
複製程式碼

####陣列型別

具有相同的維度以及元素型別的陣列,屬於同一種Class型別。

例子:

int[] a1 = new int[3];
int[] a2 = new int[4];
int[][] a3 = new int[3][4];
String[] a4 = new String[3];

System.out.println(a1.getClass());
System.out.println(a2.getClass());
System.out.println(a3.getClass());
System.out.println(a4.getClass());
複製程式碼

輸出結果:

class [I
class [I
class [[I
class [Ljava.lang.String;
複製程式碼

其中[代表是陣列型別,I代表元素型別(int)

八種基本型別的陣列不能轉換成Object陣列。因此下面的語句不合法:

Object[] objects = a1;

//因為基本型別的一維陣列不能當成Object陣列,因此只能當做是一整個陣列物件
//因此列印出來的結果是陣列的物件的值,而不是裡面的內容
//那麼,按照JDK1.5的可變長引數語法,只能解析成一個陣列物件
System.out.println(Arrays.asList(a1));
//String型別可以轉換成Object陣列列印的是內容
System.out.println(Arrays.asList(a4));
複製程式碼

####陣列

可以通過反射來對陣列進行操作,因為是Object陣列,因此不能確定整個陣列是同一個型別,因此只能確定的是每一項的型別。

private static void printObj(Object o) {
    //判斷是不是陣列型別
    if (o.getClass().isArray()) {
        //通過反射APIArray去遍歷陣列
        int length = Array.getLength(o);
        for (int i = 0; i < length; i++) {
            Object item = Array.get(o, i);
            System.out.println(item);
        }
    } else {
        System.out.println(o);
    }
}
複製程式碼

equals與hashCode的聯絡與區別。

  1. 一個類的兩個例項物件equals的時候,hashCode也必須相等,反之不成立。
  2. hashCode只有在hash集合中才有意義,比如hashMap、hashSet等。當物件被存入hash集合或者從hash集合中移除、contains方法呼叫等的時候,先會通過計算hash值,算出物件應該在的儲存區域,然後再通過在這塊儲存區域,裡面通過equals方法去判斷物件是否重複。(hash相等,equals可以不相等)

一般來說,兩個都需要重寫,而且在物件插入了hash集合以後,不要再修改這個物件與hash計算有關的數值了,因為這樣會導致hash集合根據變化之後的hash值找不到這個物件了,物件不能被清理,從而造成記憶體洩漏。

HashSet<Point> set = new HashSet<>();
Point p0 = new Point(1,2,3);
Point p1 = new Point(1,2,3);

set.add(p0);
//如果重寫了hashCode方法,這個時候不重寫equals方法,那麼這個物件可以被插入
//如果重寫了hashCode以及equals方法,那麼這個物件不可以被插入
set.add(p1);

//數值改變了,物件不能被移除(找到),從而造成記憶體洩漏
p0.x = 10000;
set.remove(p0);

System.out.println(set.size());
複製程式碼

而一般的非hash集合,例如ArrayList,只儲存資料的引用,資料是可以重複的。

Java反射的作用 -- 實現框架功能

框架與工具的區別:

相同:都是其他人提供的

不同點:

  1. 工具類是被使用者呼叫
  2. 框架是呼叫使用者提供的類

例如:

  1. 框架提供配置檔案,使用者可以配置,例如類名
  2. 讀取使用者的配置檔案
  3. 通過反射載入對應的類,並且動態去使用

配置檔案的讀取

//一定要用完整的路徑,不是硬編碼,而是運算出來的
InputStream is = new FileInputStream("檔案目錄");
Properties properties = new Properties();
properties.load(is);

String value = properties.getProperty("key");
複製程式碼

一般配置檔案的載入基本都是利用類載入器來載入。

//通過類載入器可以把與類放在一起的配置檔案讀取出來,這裡是與類相對路徑。如果寫上/代表是絕對路徑,需要寫完整/包名。。。。。
InputStream res = ReflectTest3.class.getResourceAsStream("檔案目錄");
//InputStream res = ReflectTest3.class.getClassLoader().getResourceAsStream("檔案目錄");
properties.load(res);
複製程式碼

內省,與java Bean

java bean的概念:符合一定get、set規則的java類

java bean的屬性名為get、set方法去掉get、set字首,剩下的-- 如果第二個字母也是小寫的話,那麼 -- 首字母需要變成小寫

例如:

getAge-->age

setCPU-->CPU
複製程式碼

內省:操作Java Bean物件的API

Bean b = new Bean(1);

String propertyName = "x";

//普通方法x-->X-->getX-->呼叫
//下面使用內省的方法去獲取get方法,set方法也是同一個道理
PropertyDescriptor pd = new PropertyDescriptor(propertyName, b.getClass());
Method getXMethod = pd.getReadMethod();
System.out.println(getXMethod.invoke(b, null));
複製程式碼

比較麻煩的寫法,通過BeanInfo去做

BeanInfo beanInfo = Introspector.getBeanInfo(b.getClass());
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor p : pds) {
    if (p.getName().equals("x")) {
        Method readMethod = p.getReadMethod();
        readMethod.invoke(b, null);
    }
}
複製程式碼

註解 Annotation

小知識:

  1. 型別名:形容詞+名詞
  2. 方法名:動詞+名詞

####註解的概念 註解實際上是一個類,寫註解實際上是建立註解的物件。註解相當於為程式打一種標記。javac編譯工具、開發工具以及其他程式可以利用反射來了解你的類以及各種元素上有沒有何種標記,就可以去做相應的事。

標記可以加在包、型別(Type,包括類,列舉,介面)、欄位、方法、方法的引數以及區域性變數上面。

下面是java的一些比較常見的註解:

Deprecated				//標記方法過時
Override 				//標記該方法是子類覆蓋父類的方法,告訴編譯器去檢查
SuppressWarnings 		//去掉過時的刪除線
複製程式碼

####註解的應用結構圖

一共需要三個類:

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

例子:

這是一個註解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by Administrator on 2016/9/16.
 */

@Retention(RetentionPolicy.RUNTIME)//註解保留在哪個宣告週期
@Target({ElementType.METHOD, ElementType.TYPE})//作用於什麼元素身上
public @interface A {

}
複製程式碼

註解作用的類:

@A
public class AnnotationTest {

}
複製程式碼

獲取註解(通過對AnnotationTest進行反射):

Class<AnnotationTest> c = AnnotationTest.class;
//判斷是否有對應的註解
if (c.isAnnotationPresent(A.class)) {
	//獲取註解
    A a = c.getAnnotation(A.class);
}
複製程式碼

####元註解,用來給註解新增註解的註解

@Target(ElementType.METHOD)			//宣告註解可以作用在什麼地方,特別注意TYPE是類、列舉、介面身上
@Retention(RetentionPolicy.SOURCE)	//宣告註解是在哪個宣告週期的,分別是原始碼,class檔案,執行時
複製程式碼

####注意,多個屬性的話需要用大括號括起來:

@Target({ElementType.METHOD, ElementType.TYPE})
複製程式碼

####註解的生命週期

  1. SOURCE:註解被保留到原始檔階段。當javac把原始檔編譯成.class檔案的時候,就將相應的註解去掉。例如常見的Override、SuppressWarnings都屬於SOURCE型別的生命週期,因為一旦程式碼編譯之後該註解就沒用了。
  2. CLASS:java虛擬機器通過類載入器向記憶體中載入位元組碼的時候,就將相應的註解去掉,因此無法通過反射獲取相應的註解。
  3. RUNTIME:註解保留在記憶體中的位元組碼上面了,虛擬機器在執行位元組碼的時候,仍然可以使用的註解。例如Deprecated,類被別人使用的時候,載入到記憶體,掃描,從二進位制程式碼去看是否過時,而不是檢查原始碼。

####為註解增加屬性

註解引數的可支援資料型別:

  1. 所有基本資料型別(int,float,boolean,byte,double,char,long,short)
  2. String型別
  3. Class型別
  4. enum型別
  5. Annotation型別
  6. 以上所有型別的陣列

例子:

@Retention(RetentionPolicy.RUNTIME)//註解保留在哪個宣告週期
@Target({ElementType.METHOD, ElementType.TYPE})//作用於什麼元素身上
public @interface A {

    String stringAttr();

    Class value() default Object.class;

    int[] arrAttr();

    Deprecated annoAttr() default @Deprecated;
}
複製程式碼

在使用註解的時候注意點:

  1. 當只有value需要設定值的時候(即只有value屬性或者其他屬性已經制定了default的時候),可以直接不寫value,直接在括號裡面寫值,例如:

    @SuppressWarnings("deprecation")

  2. 當型別是陣列的時候,如果元素只有一個,那麼可以省略大括號,直接寫一個元素即可。

通過反射獲得註解之後,就可以隨心去使用了:

Class<AnnotationTest> c = AnnotationTest.class;
if (c.isAnnotationPresent(A.class)) {
    A a = c.getAnnotation(A.class);
	//獲得stringAttr屬性的值
    System.out.println(a.stringAttr());
}
複製程式碼

泛型

####概念

集合,反射等等地方都使用到了泛型,免去了強制型別轉換的不安全性問題,包括code階段以及執行階段。泛型是給編譯器看的,讓編譯器攔截源程式中的非法輸入,編譯完以後就會去掉型別資訊,保證程式的執行效率。對於引數化的泛型型別,getClass方法的返回值和原始型別完全一樣。

所以編譯完以後,跳過編譯器,通過反射就可以向集合新增其他型別的資料,例子如下:

List<Integer> list = new ArrayList<>();
//通過反射的方式取新增“非法型別”到集合當中
list.getClass().getMethod("add", Object.class).invoke(list, "abc");
System.out.println(list.get(0));
複製程式碼

####關於泛型的一些術語:

ArrayList<E>						泛型型別
ArrayList<E>中的E					型別變數或者型別引數
ArrayList<String>					引數化的型別
ArrayList<String>中的String			實際型別引數
ArrayList<String>中的<>讀作type of
複製程式碼
  1. 用不用泛型,程式最終的執行結果都一樣,用了有好處而已
  2. 引數化型別,不考慮型別引數的繼承關係

例如,下面的這行程式碼是錯誤的,因為不考慮父子關係:

List<Object> list1 = new ArrayList<String>();
複製程式碼

泛型中的萬用字元 ?

不用Object,而是使用?表示任意型別。?萬用字元可以引用各種其他引數化的型別,?萬用字元定義的變數主要用作引用。型別引數沒有賦值的時候,不能呼叫與型別引數有關的方法(方法中有泛型引數的方法)。

例子:

public static void printSise(List<?> l) {

    l.add(new Object());//這一局編譯就報錯
    Object o = l.get(1);//返回值有泛型,但是我們可以轉換為Object

    for (Object obj : l) {
        System.out.println(obj);
    }
}
複製程式碼

####泛型的上下邊界,可以用&實現多個介面的限定

//上邊界
List<? extends Number> l1 = new ArrayList<Integer>();

//下邊界
List<? super Integer> l2 = new ArrayList<Object>();
複製程式碼

泛型的案例,以及各種集合(主要是MAP)的迭代方法:

####1.增強for迴圈遍歷MAP,這是最常見的並且在大多數情況下也是最可取的遍歷方式。注意:for-each迴圈在java 5中被引入所以該方法只能應用於java 5或更高的版本中。如果你遍歷的是一個空的map物件,for-each迴圈將丟擲NullPointerException,因此在遍歷前你總是應該檢查空引用。

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

for (Map.Entry<Integer, Integer> entry : map.entrySet()) {

    entry.getKey();
	entry.getValue();

}
複製程式碼

####2.在for-each迴圈中遍歷keys或values.如果只需要map中的鍵或者值,你可以通過keySet或values來實現遍歷,而不是用entrySet。該方法比entrySet遍歷在效能上稍好(快了10%),而且程式碼更加乾淨。

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

//遍歷map中的鍵

for (Integer key : map.keySet()) {

}

//遍歷map中的值

for (Integer value : map.values()) {

}
複製程式碼

####3.使用迭代器,這種方法可以在迭代之中刪除元素。

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();

while (entries.hasNext()) {

    Map.Entry<Integer, Integer> entry = entries.next();

    entry.getKey();
	entry.getValue());

}
複製程式碼

####4.通過鍵值去遍歷,效率低下,一般不採用。

Map<Integer, Integer> map = new HashMap<Integer, Integer>();

for (Integer key : map.keySet()) {

    Integer value = map.get(key);

    System.out.println("Key = " + key + ", Value = " + value);

}
複製程式碼

####總結:

如果僅需要鍵(keys)或值(values)使用方法二。如果你使用的語言版本低於java 5,或是打算在遍歷時刪除entries,必須使用方法三。否則使用方法一(鍵值都要)。

####由C++的模板函式,引入自定義泛型

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

####java中的泛型不能完全做到C++中的模板功能的原因:

java中的泛型類似於C++中的模板,但是這種相似性僅限於表面,java中的泛型基本上是在編譯器中實現,用於編譯器執行型別檢查和推斷,然後生成普通的非泛型位元組碼,這種技術成為擦除。因為擴充套件虛擬機器指令集來支援泛型被認為是無法接受的,這會為java廠商升級JVM造成困難。因此,泛型引數不同不能構成過載。

  1. 泛型的實際型別不能是基本型別,只能是引用型別。
  2. 修飾符之後,返回值型別之前,用宣告一種新的型別。
  3. 可以有多個型別變數,用逗號隔開。
  4. 型別引數的名字一般用一個大寫字母來命名。
  5. 編譯器不允許直接new T的陣列或者物件。
  6. 可以用型別變數表示異常,稱為引數化異常,用得不多。

例子:

private static <T> T[] swap(T[] arr, int i, int j) {
    T tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
    return arr;
}
複製程式碼

####引數的型別推斷(比較複雜)

型別引數的型別推斷:編譯器判斷泛型方法的實際型別引數的過程稱為型別推斷,型別推斷是相對於知覺推斷的,其實現方法是一種非常複雜的過程。根據呼叫泛型方法時實際傳遞引數型別或返回值的型別來推斷,具體規則如下:

  1. 當某個型別變數值在整個引數列表中的所有引數和返回值中的一處被應用類,那麼根據呼叫方法時該處的實際應用型別來確定,這很容易憑著感覺推斷出來,即直接根據呼叫方法時傳遞的引數型別或返回值來決定泛型引數的型別,例如: swap(new String[3],3,4) --> static void swap(E[] a,int i,int j)
  2. 當某個型別變數在整個引數列表中的所有引數和返回值中的多處被應用了,如果呼叫方法時這多處的實際應用型別都對應同一種型別來確定,這很容易憑著感覺推斷出來,例如: add(2,5) -->static T add (T a, T b)
  3. 當某個型別變數在整個引數列表中的所有引數和返回值中的多處被應用了,如果呼叫方法時這多處的實際應用型別對應到類不同的型別,且沒有使用返回值,這時候取多個引數中的最大交集型別,例如,下面語句實際對應的型別就是Number了,編譯沒問題,只是執行時出問題: fill(new Integer[3],3.5f)-->static void fill(T[], T v)//Integer∩Float = Number ,它們都是Number的子類
  4. 當某個型別變數在整個引數列表中的所有引數和返回值中的多處被應用了,如果呼叫方法時這多處的實際應用型別對應到了不同的型別,並且使用返回值,這時候優先考慮返回值的型別,例如,下面語句實際對應的型別就是Integer了,編譯將報告錯誤,將變數x的型別改為float,對比eclipse報告的錯誤提示,接著再將變數x型別改為Number,則沒有了錯誤: int x = add(3,3.5f) -->static T add(T a,T b)

####定義泛型的型別

  1. 方法級別(上面已經講過)

  2. 泛型的型別(類):多個方法使用的是同一個型別

    class A {

    }

注意,類裡面的靜態方法不能含有物件的泛型。但是可以有一般的泛型靜態方法。例子:

public class A<T> {
    //編譯器報錯,因為靜態方法可以避開物件的建立嘛
    public static void add(T t) {

    }

    //編譯器不報錯,單獨分開
    public static <E> void set(E e) {

    }
}
複製程式碼

####難點:通過反射的方法把方法中的引數的型別提取出來

public static void main(String[] args) throws Exception {

    //通過反射的方法把方法中的引數的型別提取出來
    Method method = A.class.getMethod("test", List.class);
    //其返回的是引數的引數化的型別,裡面的帶有實際的引數型別
    Type[] types = method.getGenericParameterTypes();
    //將型別向引數化型別轉換
    ParameterizedType type = (ParameterizedType) types[0];
    //得到原始型別(interface java.util.List)
    System.out.println(type.getRawType());
    //得到實際引數型別的型別(class java.lang.String)
    System.out.println(type.getActualTypeArguments()[0]);

}


public static void test(List<String> list) {

}
複製程式碼

類載入器的介紹

####類載入器的父子關係以及管轄範圍

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

例子:獲取並且列印類載入器:

public static void main(String[] args) {

    //列印出當前執行緒的類載入器
    System.out.println(Thread.currentThread().getContextClassLoader().getClass().getName());

    //第一個類預設由當前執行緒的類載入器去進行載入
    System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName());
    //System是由BootStrap類載入器載入的,是C/C++寫的,BootStrap不需要其他載入器去載入
    //在java層面不能獲取該類的引用
    System.out.println(System.class.getClassLoader() == null);

}
複製程式碼

類載入器的委託機制,相當於android中的事件傳遞,防止位元組碼的重複載入。

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

####自定義載入器

原理:ClassLoader有loadClass和findClass兩個方法,loadClass會首先去找父載入器,如果找不到就會回傳,如果傳到自己的話,就會回撥findClass方法來載入class。為了保證這一個流程不被破壞(模板方法設計模式),因此我們需要覆蓋的是findClass方法。

####自定義載入器可以實現位元組碼的加密解密

下面僅僅寫出一些關鍵的步驟:

  1. 寫一個需要被加密的類,並且編譯生成.class檔案

  2. 利用加密演算法(比如與0xff異或)對.class檔案檔案進行加密,用輸入流讀進來,再用輸出流輸出到檔案中。

  3. 自定義類載入器,繼承ClassLoader。複寫findClass方法,把加密過後的.class載入進來,轉換成位元組陣列並且解密,利用ClassLoader的下面這個方法把class檔案轉換成位元組碼。

  4. 得到位元組碼就可以通過反射的方式進行newInstance等使用了。

     //得到class檔案轉換成位元組碼
     protected final Class<?> defineClass(byte[] b, int off, int len)
    複製程式碼

###代理

要為已存在的多個具有相同介面的目標類(已經開發好,或者沒有原始碼)的各個方法增加一些系統功能,例如異常處理、日誌、計算方法的執行時間、事務管理等。可以使用代理,代理就有這樣的好處。

JVM可以在執行期間動態生成出類的位元組碼,這種動態生成的類往往用作代理,成為動態代理。JVM生成的類必須實現一個或者多個介面,所以JVM生成的類智慧用作具有相同家口的目標類的代理。

CGLIB庫可以動態生成一個類的子類,一個類的子類也可以用作該類的代理類,所以如果要為一個沒有實現介面的類生成動態代理類的話,可以使用這個庫。

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

###AOP面向切面程式設計的概念

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

####面向切面程式設計(AOP是Aspect Oriented Program的首字母縮寫),在執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計。 一般而言,我們管切入到指定類指定方法的程式碼片段稱為切面,而切入到哪些類、哪些方法則叫切入點。有了AOP,我們就可以把幾個類共有的程式碼,抽取到一個切片中,等到需要時再切入物件中去,從而改變其原有的行為。 這樣看來,AOP其實只是OOP的補充而已。OOP從橫向上區分出一個個的類來,而AOP則從縱向上向物件中加入特定的程式碼。有了AOP,OOP變得立體了。如果加上時間維度,AOP使OOP由原來的二維變為三維了,由平面變成立體了。從技術上來說,AOP基本上是通過代理機制實現的。 AOP在程式設計歷史上可以說是里程碑式的,對OOP程式設計是一種十分有益的補充。

####例子

使用反射API,JVM動態生成Collection介面的代理類

//使用反射API,JVM動態生成Collection介面的代理類
Class<?> clazzProxy0 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);

//輸入這個動態生成的類的名字
System.out.println(clazzProxy0.getName());

//輸出所有構造方法
printConstructor(clazzProxy0);

//輸出所有方法
printMethod(clazzProxy0);

//使用這個類去構建物件
//不能使用無引數的建構函式,因為代理類只有一個有引數的建構函式
//clazzProxy0.newInstance();

Constructor<?> constructor = clazzProxy0.getConstructor(InvocationHandler.class);
Collection collection = (Collection) constructor.newInstance(new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
});
複製程式碼

生成代理類物件需要傳入InvocationHandler物件,代理類的方法呼叫會觸發InvocationHandler的分發,InvocationHandler內部會對被代理類的物件的方法進行呼叫,並且插入一些指定的功能。

下面是列印所有構造方法與方法的函式

private static void printMethod(Class<?> clazz) {
    System.out.println("所有方法");
    for (Method m : clazz.getMethods()) {
        StringBuilder builder = new StringBuilder(m.getName());
        builder.append("(");

        Class[] types = m.getParameterTypes();
        for (Class<?> t : types) {
            builder.append(t.getName()).append(",");
        }
        if (types.length != 0) {
            builder.deleteCharAt(builder.length() - 1);
        }

        builder.append(")");

        System.out.println(builder.toString());
    }
}

private static void printConstructor(Class<?> clazz) {
    System.out.println("所有構造方法");
    for (Constructor c : clazz.getConstructors()) {
        StringBuilder builder = new StringBuilder(c.getName());
        builder.append("(");

        Class[] types = c.getParameterTypes();
        for (Class<?> t : types) {
            builder.append(t.getName()).append(",");
        }
        if (types.length != 0) {
            builder.deleteCharAt(builder.length() - 1);
        }

        builder.append(")");

        System.out.println(builder.toString());
    }
}
複製程式碼

一步到位:

//一步到位,獲取代理類並且生成物件
Collection collection1 = (Collection) Proxy.newProxyInstance(Collection.class.getClassLoader(), new Class[]{Collection.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
});
複製程式碼

下面給出實用的案例,在每個方法的呼叫的時候插入廣告:

/**
 * Created by Administrator on 2016/9/18.
 */
public class ProxyTest1 {

    //被代理的物件
    private static ArrayList<String> target = new ArrayList<>();

    public static void main(String[] args) {

        //一步到位,獲取代理類並且生成物件
        Collection collection = (Collection) Proxy.newProxyInstance(Collection.class.getClassLoader(), new Class[]{Collection.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                System.out.println("----廣告開始----");

                Object returnVal = method.invoke(target, args);

                System.out.println("----廣告結束----");

                return returnVal;


            }
        });

        collection.add("aad");
        collection.add("aad");
        collection.add("aad");
        collection.add("aad");
        System.out.println(collection.size());
    }

}
複製程式碼

當然,我們希望的是呼叫的東西是框架完成以後使用者(配置)輸入的,因此,我們需要再提供介面:

public interface Advice {

    void before(Object proxy, Method method, Object[] args);

    void after(Object proxy, Method method, Object[] args);
}
複製程式碼

如上所示,為了方便,介面只提供兩個簡單的方法,分別在方法執行前後執行。

然後,我們也把獲取代理物件的方法封裝一下,使用者只需要傳入介面的實現類即可。

public class ProxyTest1 {

    //被代理的物件
    private static ArrayList<String> target = new ArrayList<>();

    public static void main(String[] args) {

        //一步到位,獲取代理類並且生成物件
        Collection collection = (Collection) getProxyInstance(Collection.class, new Advice() {
            @Override
            public void before(Object proxy, Method method, Object[] args) {
                System.out.println(method.getName() + "開始執行");
            }

            @Override
            public void after(Object proxy, Method method, Object[] args) {
                System.out.println(method.getName() + "結束執行");
            }
        });

        collection.add("aad");
        collection.size();
    }

    private static Object getProxyInstance(Class<?> clazz, Advice advice) {
        return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                advice.before(proxy, method, args);
                Object returnVal = method.invoke(target, args);
                advice.after(proxy, method, args);

                return returnVal;	
            }
        });
    }

}
複製程式碼

注意:介面的方法都會交給InvocationHandler分發處理,但是由Object繼承過來的方法只有toString、equals、hashCode才會交給InvocationHandler處理。

###String、StringBuilder與StringBuffer的區別

  1. 在執行速度方面的比較:StringBuilder > StringBuffer

  2. StringBuffer與StringBuilder,他們是字串變數,是可改變的物件,每當我們用它們對字串做操作時,實際上是在一個物件上操作的,不像String一樣建立一些物件進行操作,所以速度就快了。

  3. StringBuilder:執行緒非安全的   StringBuffer:執行緒安全的

    當我們在字串緩衝去被多個執行緒使用是,JVM不能保證StringBuilder的操作是安全的,雖然他的速度最快,但是可以保證StringBuffer是可以正確操作的。當然大多數情況下就是我們是在單執行緒下進行的操作,所以大多數情況下是建議用StringBuilder而不用StringBuffer的,就是速度的原因。

對於三者使用的總結:

  1. 如果要操作少量的資料用 = String
  2. 單執行緒操作字串緩衝區 下操作大量資料 = StringBuilder
  3. 多執行緒操作字串緩衝區 下操作大量資料 = StringBuffer

###Overload(過載)與Override(重寫)的區別

  1. 過載是指不同的函式使用相同的函式名,但是函式的引數個數或型別不同。呼叫的時候根據函式的引數來區別不同的函式。

  2. 覆蓋(也叫重寫)是指在派生類中重新對基類中的虛擬函式(注意是虛擬函式)重新實現。即函式名和引數都一樣,只是函式的實現體不一樣。

  3. 隱藏是指派生類中的函式把基類中相同名字的函式遮蔽掉了。

  4. 方法的重寫(Overriding)和過載(Overloading)是Java多型性的不同表現。 重寫(Overriding)是父類與子類之間多型性的一種表現,而過載(Overloading)是一個類中多型性的一種表現。

    override(重寫)

     1. 方法名、引數、返回值相同。
     2. 子類方法不能縮小父類方法的訪問許可權。	
     3. 子類方法不能丟擲比父類方法更多的異常(但子類方法可以不丟擲異常)。	
     4. 存在於父類和子類之間。
     5. 方法被定義為final不能被重寫。
    複製程式碼

    overload(過載)

     1. 引數型別、個數、順序至少有一個不相同。 
     2. 不能過載只有返回值不同的方法名。
     3. 存在於父類和子類、同類中。
    複製程式碼

==、hashCode與equals的區別:

  1. 基本資料型別,也稱原始資料型別。byte,short,char,int,long,float,double,boolean他們之間的比較,應用雙等號(==),比較的是他們的值。
  2. 複合資料型別(類)複合資料型別之間進行equals比較,在沒有覆寫equals方法的情況下,他們之間的比較還是基於他們在記憶體中的存放位置的地址值的,因為Object的equals方法也是用雙等號(==)進行比較的,所以比較後的結果跟雙等號(==)的結果相同。
  3. 將物件放入到集合中時,首先判斷要放入物件的hashcode值與集合中的任意一個元素的hashcode值是否相等,如果不相等直接將該物件放入集合中。如果hashcode值相等,然後再通過equals方法判斷要放入物件與集合中的任意一個物件是否相等,如果equals判斷不相等,直接將該元素放入到集合中,否則不放入。

規則:

  1. 如果兩個物件根據equals()方法比較是相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法都必須產生同樣的整數結果。
  2. 如果兩個物件根據equals()方法比較是不相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法,則不一定要產生相同的整數結果。但是程式設計師應該知道,給不相等的物件產生截然不同的整數結果,有可能提高雜湊表的效能。

HashMap、HashTable的區別

  1. HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。
  2. HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是執行緒安全的,多個執行緒可以共享一個Hashtable;而如果沒有正確的同步的話,多個執行緒是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴充套件性更好。
  3. 兩者使用的迭代方式不一樣。

sychronized意味著在一次僅有一個執行緒能夠更改Hashtable。就是說任何執行緒要更新Hashtable時要首先獲得同步鎖,其它執行緒要等到同步鎖被釋放之後才能再次獲得同步鎖更新Hashtable。

HashMap可以通過下面的語句進行同步: Map m = Collections.synchronizeMap(hashMap);

總結:

Hashtable和HashMap有幾個主要的不同:執行緒安全以及速度。僅在你需要完全的執行緒安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

ArrayList、Vector的區別

  1. Vector是執行緒安全的,也就是說是它的方法之間是執行緒同步的,而ArrayList是執行緒序不安全的。
  2. 資料增長:ArrayList與Vector都有一個初始的容量大小,當儲存進它們裡面的元素的個數超過了容量時,就需要增加ArrayList與Vector的儲存空間,每次要增加儲存空間時,不是隻增加一個儲存單元,而是增加多個儲存單元,每次增加的儲存單元的個數在記憶體空間利用與程式效率之間要取得一定的平衡。Vector預設增長為原來兩倍,而ArrayList的增長策略在文件中沒有明確規定(從原始碼看到的是增長為原來的1.5倍)。ArrayList與Vector都可以設定初始的空間大小,Vector還可以設定增長的空間大小,而ArrayList沒有提供設定增長空間的方法。

###Java中建立物件的四種方法 收藏Java中建立物件的四種方式

  1. 用new語句建立物件,這是最常見的建立物件的方法。
  2. 運用反射手段,呼叫java.lang.Class或者java.lang.reflect.Constructor類的newInstance()例項方法。
  3. 呼叫物件的clone()方法。
  4. 運用反序列化手段,搜尋呼叫java.io.ObjectInputStream物件的 readObject()方法。

###說說靜態變數、靜態程式碼塊載入的過程和時機? 回答:當類載入器將類載入到JVM中的時候就會建立靜態變數,靜態變數載入的時候就會分配記憶體空間。靜態程式碼塊的程式碼只會在類第一次初始化,也就是第一次被使用的時候執行一次。

##MVC、MVVP、MVP設計模式

  1. MVC:MVC的耦合性還是比較高,View可以直接訪問Model
  2. MVP:Model、View、Presenter,View不能直接訪問Model
  3. MVVM:View和Model的雙向繫結,類似於Data-Binding

###JAVA容器的架構圖

JAVA面試 基礎加強與鞏固:反射、註解、泛型等

如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:

公眾號:Android開發進階

我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)

相關文章