錘子剪刀布 (20分)
泛型
一、為什麼要存在泛型
泛型特性對Java影響最大的是集合框架的使用,因為Java5增加泛型的支援在很大程度上都是為了集合能夠記住其元素的資料型別。
public class demo { public static void main(String[] args) { List list = new ArrayList(); list.add("java基礎"); list.add(666); list.forEach(a -> System.out.println(((String)a).length())); } }
上面程式先是建立了一個List
集合,但是我們一開覺得我們只會存放字串型的資料物件,突然你加入了一個整型資料,我們在這個地方加入的物件做限制,所以我們可以在add
的時候把整型資料加進去,但是我們如果在使用的時候用到了字串型別的方法,它就會出現強制型別轉化錯誤。
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
增加了泛型支援的集合,完全可以記住集合中元素的型別,並且可以在編譯時檢查集合中元素型別是否符合要求,如果我試圖想集合中新增不符合我規定的資料型別的時候,編譯器就會提示錯誤。增加泛型後的集合,可以讓程式碼更加簡潔,程式更加健壯,我們如果在編譯時沒有發出警告,執行時候就不會產生ClassCastException異常。
二、使用
從Java5之後,Java引入了引數化型別
的概念,就是允許程式在建立集合時指定集合中元素的型別,例如:List<Stirng>
,這個意思就是這個List集合中只能儲存字元型物件。
在java中引數化的型別被稱為泛型(Generic)
public class demo { public static void main(String[] args) { // 新增泛型,只能儲存字串 List<String> list = new ArrayList<String>(); list.add("java基礎"); list.add("4444"); list.forEach(a -> System.out.println(((String)a).length())); } }
我們在上面建立了一個特殊的List
集合,這個集合只能儲存字串,不能儲存其他型別。
建立這個特殊集合的方法:在集合介面、類後增加尖括號<>
,尖括號裡面放一個數據型別,就相當於表明了這個集合介面、集合類只能儲存特定的物件。
三、Java7之後增強寫法 --- 菱形語法
在Java7之前,如果使用帶泛型的介面、類定義變數的時候,那麼構造器建立物件的時候後面也必須帶上泛型,程式會顯得冗餘。例如:
List<String> list = new ArrayList<String>();
上面這個我們就可以改變一下,去除尖括號裡面的內容。Java7以前的時候尖括號裡面的泛型是必須帶著的,Java7之後是可以不帶著,省略裡面的泛型,只留下尖括號。java可以推斷出尖括號裡面的泛型資訊。由於尖括號像菱形,所以我們把這個稱作為菱形語法。
List<String> list = new ArrayList<>();
從上面對比我們可以看的出來,菱形語法沒有對原來的泛型格式做出改變,只是簡化了泛型變成時候的程式碼。
public interface Foo<T> {
void test(T t);
}
public class AnonymousTest {
public static void main(String[] args) {
Demo demo = new Demo();
Foo<String> f = new Foo<>() {
@Override
public void test(String s) {
}
};
}
}
java9再次增強了“菱形”語法,允許建立匿名內部類的時候使用菱形語法,java可以根據上下文推斷出匿名內部類中的泛型型別。
四、再次瞭解泛型
我們檢視List
集合的原始碼,他們在裡面使用了泛型,
public interface List<E> extends Collection<E> {
......
Iterator<E> iterator();
......
}
1、定義泛型介面、類
所謂的泛型,就是允許在定義類、介面、方法的時候使用型別形參(也就是泛型),這個型別形參將在宣告變數、建立物件、呼叫方法的時候可以動態的指定(也就是傳入實際的引數),Java5改寫了集合框架中的全部介面和類,為這些介面、類增加了泛型支援,從而可以在宣告變數、建立物件的時候傳入型別實參,這個類似於List<String>
。
我們可以想象一下,接口裡面的所有E
被替換成了String
,就類似於產生了一個新型別:List<String>
.
public interface List<String> extends List{
...
Iterator<String> iterator();
...
default void sort(Comparator<? super String> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<String> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((String) e);
}
}
...
}
通過這種方式,雖然我們只定義了一個List<E>
介面,但在實際使用時會產生若干個List介面,我們只需要為E
傳入不同的型別實參就行了,系統就會多出一個List子介面。
需要注意的是
LIst<Stirng>
不會被替換掉,系統沒有進行原始碼複製,二進位制程式碼中也沒有,磁碟中也沒有,記憶體中也沒有
public class Apple<T> {
// 使用T型別定義例項變數
private T info;
public Apple(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
public static void main(String[] args) {
Apple<String> a1 = new Apple<>("蘋果");
System.out.println(a1.getInfo());
Apple<Double> a2 = new Apple<>(6.66);
System.out.println(a2.getInfo());
}
}
上面程式定義了一個帶泛型宣告的Apple<T>
類(不用理會這個泛型形參是否具有實際的意義),使用Apple<T>
類時就可以為T
型別傳入實際型別,這樣就可以生成Apple<String>
、Apple<Double>
.....形式的多個邏輯子類(物理上是不存在的)。
注意:當建立帶泛型宣告的自定義類時,為該類定義構造器的時候,構造器還是原來的類名,不要增加泛型宣告。例如:
Apple<T>
類的構造器,構造名還是Apple,而不是Apple<T>
,呼叫時卻可以使用Apple<t>
的形式,當然應該為T傳入對應的實參,順便我們使用了菱形語法。
2、從定義的泛型類中派生出子類
當我們建立帶有泛型宣告的介面、父類後,實現介面的子類、繼承父類的子類,不應該再包含泛型形參,應該指定需要的實參。
下面的程式碼就是錯誤的:
public class A1 extends Apple<T> {
.......
}
正確寫法:(為T指定對應的實參,比如String)
public class A2 extends Apple<String> {
public A2(String info) {
super(info);
}
}
子類也可以不帶泛型宣告的實參:
public class A1 extends Apple {
public A1(Object info) {
super(info);
}
}
像上面這種使用Apple類時省略泛型的形式被稱為原始型別。
public class A2 extends Apple<String> {
public A2(String info) {
super(info);
}
@Override
public String getInfo() {
return "子類: " + super.getInfo();
}
}
如果子類重寫父類的方法,那麼在所有T
型別的地方都會被替換成String
型別,如果不替換,編譯不會通過你的程式碼。
public class TestGeneric {
public static void main(String[] args) {
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());
}
}
l1
、l2
通過new ArrayList<>()
產生,我們直觀的看,通過getClass()
方法來對比,一開始我們會認為輸出false
,但是實際上輸出的是true
,因為不管泛型的實際型別引數是什麼,它們在執行時總有同樣的類(class)。對於Java而言,它們依然被當成同一個類來處理,在記憶體中也只佔用一塊記憶體空間。所以我們在靜態的方法、靜態初始塊、靜態變數中不允許使用泛型形參。
以下是錯誤例項:
public class TestGeneric {
//static T info;
//public static void test(T t){}
}
五、型別萬用字元
當使用一個泛型類時(包括宣告變數和建立物件),都應該為這個泛型類傳入一個型別實參。
假設我們需要定義一個方法,這個方法裡面有集合形參,如果集合形參的元素型別不確定,我們該怎麼定義???
public void test (List c) {
for (int i=0; i<c.size(); i++) {
System.out.println(c.get(i));
}
}
上面的程式這麼寫是沒有問題的,這是最簡單的List集合的遍歷。由於List
是一個有泛型宣告的介面,我們在這個地方卻沒有為它傳入實際的型別引數,這會引發泛型警告。所以我們考慮傳入一個實參----Object
。
public void test (List<Object> c) {
for (int i=0; i<c.size(); i++) {
System.out.println(c.get(i));
}
}
表面上看起來這個方法是沒有問題的,但是在呼叫的時候就會出現test (java.util.List-sjiava.lang.object>) in Test cannot be appliedto (java.util.List<java.lang.String>
也就是無法將String應用到Object。這是由於實際傳入的型別引數不是我們所期望的。
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
test(stringList);
}
上面程式顯而易見出現編譯錯誤,這表明List<String>
物件不能被當成List<object>
來處理,也就是說List<String>
不是List<object>
的子類。
與陣列做對比,我們先定義了一個Integer
型別的陣列,然後我們整數型陣列賦給了Number
陣列,但是我們在賦值操作時,給了一個Double
型別數字,編譯器沒有報錯,但是你在執行時就會丟擲Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double
,陣列儲存異常,這就是一種潛在的風險。
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
// test(stringList);
Integer[] ia = new Integer[5];
Number[] na = ia;
// Storing element of type "java.lang.Double'
// to array of java.lang.Integer' elements may produce 'ArrayStoreException
na[0] = 0.5;
}
一門優秀的設計語言,除了功能強大,還能提供強大的“錯誤提示”和“出錯警告”,我們才能避免開發者犯錯,但是java允許
Integer[]
賦值給Number[]
顯然不是一個安全的設計。
所以在泛型設計的時候做出了改進,不允許List<Integer>
賦值給List<Number>
變數.。
public static void main(String[] args) {
// 此處會引發編譯錯誤
List<Integer> la = new ArrayList<>();
List<Number> lm = la;
}
java泛型設計的原則:只要程式碼在編譯時不出現警告,就不會遇到執行時ClassCastExeption
異常,也就是型別轉換異常。
1、使用型別萬用字元
型別萬用字元:?
,可以匹配任何型別。
為了表示各種泛型的List
的父類,我們可以使用這個?
萬用字元,將問號作為一個型別實參傳給List
集合,形式:List<?>
那麼上面那個List<Object>
的那個方法就可以這麼改:
public static void test (List<?> c) {
for (int i=0; i<c.size(); i++) {
System.out.println(c.get(i));
}
}
現在你是用任何型別的List
來呼叫它,程式依然可以訪問集合c
中的元素,其型別是Object
,安全,因為不管List
的真實型別是什麼,它包含都是Object
。
List<?> c = new ArrayList<>();
// 下面這行程式碼會引起編譯錯誤,null除外
c.add(new Object());
上面的程式無法確定c
集合的元素型別,所以無法像其中新增物件,根據List<E>
介面定義處的程式碼可以發現:add()
方法有型別引數E
作為集合的元素型別,所以傳給add
的引數必須是E類的物件或者其子類的物件。但是在上面的例子中我們不知道E
是什麼,所以程式無法將任何物件“丟進”這個集合。唯一的例外是null
,因為它是所有引用型別的例項。
2、設定型別萬用字元的上限
當我們使用List<?>
這種形式的時候,表明這個List
集合是任何泛型List
的父類,但是還有一種特殊的情況:設計的程式不希望這個List<?>
是任何List
的父類,只希望它代表某一大類的List
的父類。
先寫三個形狀類:
public abstract class Shape {
public abstract void draw(Canvas canvas);
}
public class Rectangle extends Shape {
@Override
public void draw(Canvas canvas) {
System.out.println("矩形:" + canvas + "上");
}
}
public class Circle extends Shape {
@Override
public void draw(Canvas canvas) {
System.out.println("圓形: " + canvas + "上");
}
}
然後在寫一個畫布類及方法:
public class Canvas {
public void drawAll (List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
List<Circle> circles = new ArrayList<>();
circles.add(new Circle());
Canvas c = new Canvas();
// 此處會引發編譯錯誤,這是由於 List<Circle>不是List<Shape>的子類
c.drawAll(circles);
}
}
那麼我們可以這麼改,設定一個萬用字元上限:
public void drawAll (List<? extends Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
List<? extends Shape>
是受限制萬用字元的例子,此處的問號是代表未知型別,就像前面看的萬用字元一樣,但是此處的未知型別要是Shape
的子型別,那麼稱Shape
是萬用字元的上限。
3、設定型別萬用字元的下限
<? super 型別>
目的:賦值元素到目的集合中,我們必須保證目的集合比源集合的大才行。
public class Utils {
public static void main(String[] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(666);
li.add(888);
// li ---> ln copy(ln , li)
Integer i = copy(ln, li);
System.out.println(ln);
}
public static <T> T copy(List<? super T> dest, List<T> src){
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
}
4、設定泛型形參的上限
public class Apple<T extends Number> {
......
}
六、泛型方法
1、定義泛型方法
在有些特定的情況下,我們在定義類、介面的時候沒有使用泛型形參,但是在定義方法的時候想自己定義泛型形參,也是可以的。
需求: 將一個Object陣列的所有元素新增到一個Collection集合當中。
static void fromArrayToCollection(Object[] a, Collection<Object> c){
for (Object o : a) {
c.add(a);
}
}
上面方法侷限性強,如果傳入Stirng型別的集合就不能通過編譯。例如:
public static void main(String[] args) {
Object object = new Object();
Object[] arr = new Object[4];
String[] str = {"a", "b"};
Collection<String> c = new ArrayList<>();
// 出錯
fromArrayToCollection(str,c);
}
泛型方法格式:
修飾符 <T, S> 返回值型別 方法名(形參列表){
......
}
那麼我們就可以這麼改:
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for (T cc : a) {
c.add(cc);
}
}
2、泛型方法和型別萬用字元的區別
在大多數情況下,可以使用型別方法代替型別萬用字元。
3、菱形語法失效
public class Test {
public static void main(String[] args) {
MyClass<String> mc1 = new MyClass<>("rrrr");
MyClass<String> mc2 = new <String> MyClass<String>("fff");
}
}
由於你顯式的指定了構造器的泛型形參,所以菱形語法失效。