Java新特性學習筆記
第十二章 泛型
12.1 泛型的概念
在Java中我們在宣告方法時,當在完成方法功能時如果有未知的資料需要參與,這些未知的資料需要在呼叫方法時才能確定,那麼我們把這樣的資料通過形參表示。那麼在方法體中,用這個形參名來代表那個未知的資料,而呼叫者在呼叫時,對應的傳入值就可以了。
受以上兩點啟發,JDK1.5設計了泛型的概念。泛型即為“型別引數”,這個型別引數在宣告它的類、介面或方法中,代表未知的通用的型別。例如:
java.lang.Comparable介面和java.util.Comparator介面,是用於物件比較大小的規範介面,這兩個介面只是限定了當一個物件大於另一個物件時返回正整數,小於返回負整數,等於返回0。但是並不確定是什麼型別的物件比較大小,之前的時候只能用Object型別表示,使用時既麻煩又不安全,因此JDK1.5就給它們增加了泛型。
public interface Comparable<T>{
int compareTo(T o) ;
}
public interface Comparator<T>{
int compare(T o1, T o2) ;
}
其中就是型別引數,即泛型。
12.1.2 泛型的好處
示例程式碼:
JavaBean:圓型別
class Circle{
private double radius;
public Circle(double radius) {
super();
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public String toString() {
return "Circle [radius=" + radius + "]";
}
}
比較器
import java.util.Comparator;
public class CircleComparator implements Comparator {
@Override
public int compare(Object o1, Object o2) {
//強制型別轉換
Circle c1 = (Circle) o1;
Circle c2 = (Circle) o2;
return Double.compare(c1.getRadius(), c2.getRadius());
}
}
測試類
public class TestGeneric {
public static void main(String[] args) {
CircleComparator com = new CircleComparator();
System.out.println(com.compare(new Circle(1), new Circle(2)));
System.out.println(com.compare("圓1", "圓2"));//執行時異常:ClassCastException
}
}
那麼我們在使用如上面這樣的介面時,如果沒有泛型或不指定泛型,很麻煩,而且有安全隱患。
因為在設計(編譯)Comparator介面時,不知道它會用於哪種型別的物件比較,因此只能將compare方法的形參設計為Object型別,而實際在compare方法中需要向下轉型為Circle,才能呼叫Circle類的getRadius()獲取半徑值進行比較。
使用泛型:
比較器:
class CircleComparator implements Comparator<Circle>{
@Override
public int compare(Circle o1, Circle o2) {
//不再需要強制型別轉換,程式碼更簡潔
return Double.compare(o1.getRadius(), o2.getRadius());
}
}
測試類
import java.util.Comparator;
public class TestGeneric {
public static void main(String[] args) {
CircleComparator com = new CircleComparator();
System.out.println(com.compare(new Circle(1), new Circle(2)));
// System.out.println(com.compare("圓1", "圓2"));//編譯錯誤,因為"圓1", "圓2"不是Circle型別,是String型別,編譯器提前報錯,而不是冒著風險在執行時再報錯
}
}
如果有了泛型並使用泛型,那麼既能保證安全,又能簡化程式碼。
因為把不安全的因素在編譯期間就排除了;既然通過了編譯,那麼型別一定是符合要求的,就避免了型別轉換。
12.1.3 泛型的相關名詞
<型別>這種語法形式就叫泛型。
其中:
-
是型別變數(Type Variables),而是代表未知的資料型別,我們可以指定為,,
等,那麼<型別>的形式我們成為型別引數; - 類比方法的引數的概念,我們可以把,稱為型別形參,將
稱為型別實參,有助於我們理解泛型;
- 類比方法的引數的概念,我們可以把,稱為型別形參,將
-
Comparator這種就稱為引數化型別(Parameterized Types)。
自從有了泛型之後,Java的資料型別就更豐富了:
Class:Class
類的例項表示正在執行的 Java 應用程式中的類和介面。列舉是一種類,註釋是一種介面。每個陣列屬於被對映為 Class 物件的一個類,所有具有相同元素型別和維數的陣列都共享該 Class
物件。基本的 Java 型別(boolean
、byte
、char
、short
、int
、long
、float
和 double
)和關鍵字 void
也表示為 Class
物件。
- GenericArrayType:泛化的陣列型別,即T[]
- ParameterizedType:引數化型別,例如:Comparator,Comparator
- TypeVariable:型別變數,例如:Comparator中的T,Map<K,V>中的K,V
- WildcardType:萬用字元型別,例如:Comparator<?>等
12.1.4 在哪裡可以宣告型別變數<T>
- 宣告類或介面時,在類名或介面名後面宣告型別變數,我們把這樣的類或介面稱為泛型類或泛型介面
【修飾符】 class 類名<型別變數列表> 【extends 父類】 【implements 父介面們】{
}
【修飾符】 interface 介面名<型別變數列表> 【implements 父介面們】{
}
例如:
public class ArrayList<E>
public interface Map<K,V>{
....
}
- 宣告方法時,在【修飾符】與返回值型別之間宣告型別變數,我們把宣告(是宣告不是單純的使用)了型別變數的方法稱為泛型方法
【修飾符】 <型別變數列表> 返回值型別 方法名(【形參列表】)【throws 異常列表】{
//...
}
例如:java.util.Arrays類中的
public static <T> List<T> asList(T... a){
....
}
12.2 引數型別:泛型類與泛型介面
當我們在宣告類或介面時,類或介面中定義某個成員時,該成員有些型別是不確定的,而這個型別需要在使用這個類或介面時才可以確定,那麼我們可以使用泛型。
12.2.1 宣告泛型類與泛型介面
語法格式:
【修飾符】 class 類名<型別變數列表> 【extends 父類】 【implements 父介面們】{
}
【修飾符】 interface 介面名<型別變數列表> 【implements 父介面們】{
}
注意:
- <型別變數列表>:可以是一個或多個型別變數,一般都是使用單個的大寫字母表示。例如:、<K,V>等。
- <型別變數列表>中的型別變數不能用於靜態成員上。
什麼時候使用泛型類或泛型介面呢?
- 當某個類的非靜態例項變數的型別不確定,需要在建立物件或子類繼承時才能確定
- 當某個(些)類的非靜態方法的形參型別不確定,需要在建立物件或子類繼承時才能確定
示例程式碼:
例如:我們要宣告一個學生類,該學生包含姓名、成績,而此時學生的成績型別不確定,為什麼呢,因為,語文老師希望成績是“優秀”、“良好”、“及格”、“不及格”,數學老師希望成績是89.5, 65.0,英語老師希望成績是’A’,‘B’,‘C’,‘D’,‘E’。那麼我們在設計這個學生類時,就可以使用泛型。
public class Student<T>{
private String name;
private T score;
public Student() {
super();
}
public Student(String name, T score) {
super();
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScore() {
return score;
}
public void setScore(T score) {
this.score = score;
}
@Override
public String toString() {
return "姓名:" + name + ", 成績:" + score;
}
}
12.2.2 使用泛型類與泛型介面
在使用這種引數化的類與介面時,我們需要指定泛型變數的實際型別引數:
(1)實際型別引數必須是引用資料型別,不能是基本資料型別
(2)在建立類的物件時指定型別變數對應的實際型別引數
public class TestGeneric{
public static void main(String[] args) {
//語文老師使用時:
Student<String> stu1 = new Student<String>("張三", "良好");
//數學老師使用時:
//Student<double> stu2 = new Student<double>("張三", 90.5);//錯誤,必須是引用資料型別
Student<Double> stu2 = new Student<Double>("張三", 90.5);
//英語老師使用時:
Student<Character> stu3 = new Student<Character>("張三", 'C');
//錯誤的指定
//Student<Object> stu = new Student<String>();//錯誤的
}
}
JDK1.7支援簡寫形式:Student stu1 = new Student<>(“張三”, “良好”);
指定泛型實參時,必須左右兩邊一致,不存在多型現象
(3)在繼承泛型類或實現泛型介面時,指定型別變數對應的實際型別引數
class ChineseStudent extends Student<String>{
public ChineseStudent() {
super();
}
public ChineseStudent(String name, String score) {
super(name, score);
}
}
public class TestGeneric{
public static void main(String[] args) {
//語文老師使用時:
ChineseStudent stu = new ChineseStudent("張三", "良好");
}
}
class Circle implements Comparable<Circle>{
private double radius;
public Circle(double radius) {
super();
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public String toString() {
return "Circle [radius=" + radius + "]";
}
@Override
public int compareTo(Circle c){
return Double.compare(radius,c.radius);
}
}
12.2.3 型別變數的上限
當在宣告型別變數時,如果不希望這個型別變數代表任意引用資料型別,而是某個系列的引用資料型別,那麼可以設定型別變數的上限。
語法格式:
<型別變數 extends 上限>
如果有多個上限
<型別變數 extends 上限1 & 上限2>
如果多個上限中有類有介面,那麼只能有一個類,而且必須寫在最左邊。介面的話,可以多個。
如果在宣告<型別變數>時沒有指定任何上限,預設上限是java.lang.Object。
例如:我們要宣告一個兩個數求和的工具類,要求兩個加數必須是Number數字型別,並且實現Comparable介面。
class SumTools<T extends Number & Comparable<T>>{
private T a;
private T b;
public SumTools(T a, T b) {
super();
this.a = a;
this.b = b;
}
@SuppressWarnings("unchecked")
public T getSum(){
if(a instanceof BigInteger){
return (T) ((BigInteger) a).add((BigInteger)b);
}else if(a instanceof BigDecimal){
return (T) ((BigDecimal) a).add((BigDecimal)b);
}else if(a instanceof Integer){
return (T)(Integer.valueOf((Integer)a+(Integer)b));
}else if(a instanceof Long){
return (T)(Long.valueOf((Long)a+(Long)b));
}else if(a instanceof Float){
return (T)(Float.valueOf((Float)a+(Float)b));
}else if(a instanceof Double){
return (T)(Double.valueOf((Double)a+(Double)b));
}
throw new UnsupportedOperationException("不支援該操作");
}
}
測試類
public static void main(String[] args) {
SumTools<Integer> s = new SumTools<Integer>(1,2);
Integer sum = s.getSum();
System.out.println(sum);
// SumTools<String> s = new SumTools<String>("1","2");//錯誤,因為String型別不是extends Number
}
12.2.4 泛型擦除
當使用引數化型別的類或介面時,如果沒有指定泛型,那麼會怎麼樣呢?
會發生泛型擦除,自動按照最左邊的第一個上限處理。如果沒有指定上限,上限即為Object。
public static void main(String[] args) {
SumTools s = new SumTools(1,2);
Number sum = s.getSum();
System.out.println(sum);
}
import java.util.Comparator;
public class CircleComparator implements Comparator{
@Override
public int compare(Object o1, Object o2) {
//強制型別轉換
Circle c1 = (Circle) o1;
Circle c2 = (Circle) o2;
return Double.compare(c1.getRadius(), c2.getRadius());
}
}
12.3 泛型方法
前面介紹了在定義類、介面時可以宣告<型別變數>,在該類的方法和屬性定義、介面的方法定義中,這些<型別變數>可被當成普通型別來用。但是,在另外一些情況下,
(1)如果我們定義類、介面時沒有使用<型別變數>,但是某個方法形參型別不確定時,可以單獨這個方法定義<型別變數>;
(2)另外我們之前說類和介面上的型別形參是不能用於靜態方法中,那麼當某個靜態方法的形參型別不確定時,可以單獨定義<型別變數>。
那麼,JDK1.5之後,還提供了泛型方法的支援。
語法格式:
【修飾符】 <型別變數列表> 返回值型別 方法名(【形參列表】)【throws 異常列表】{
//...
}
- <型別變數列表>:可以是一個或多個型別變數,一般都是使用單個的大寫字母表示。例如:、<K,V>等。
- <型別變數>同樣也可以指定上限
示例程式碼:
我們編寫一個數組工具類,包含可以給任意物件陣列進行從小到大排序,要求陣列元素型別必須實現Comparable介面
public class MyArrays{
public static <T extends Comparable<T>> void sort(T[] arr){
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length-i; j++) {
if(arr[j].compareTo(arr[j+1])>0){
T temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
12.4 型別萬用字元
當我們宣告一個變數/形參時,這個變數/形參的型別是一個泛型類或泛型介面,例如:Comparator型別,但是我們仍然無法確定這個泛型類或泛型介面的型別變數的具體型別,此時我們考慮使用型別萬用字元。
例如:
這個學生類是一個引數化的泛型類,程式碼如下(詳細請看$12.2.1中的示例說明):
public class Student<T>{
private String name;
private T score;
public Student() {
super();
}
public Student(String name, T score) {
super();
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScore() {
return score;
}
public void setScore(T score) {
this.score = score;
}
@Override
public String toString() {
return "姓名:" + name + ", 成績:" + score;
}
}
12.4.1 <?>任意型別
例如:我們要宣告一個學生管理類,這個管理類要包含一個方法,可以遍歷學生陣列。
學生管理類:
class StudentService {
public static void print(Student<?>[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
測試類
public class TestGeneric {
public static void main(String[] args) {
// 語文老師使用時:
Student<String> stu1 = new Student<String>("張三", "良好");
// 數學老師使用時:
// Student<double> stu2 = new Student<double>("張三", 90.5);//錯誤,必須是引用資料型別
Student<Double> stu2 = new Student<Double>("張三", 90.5);
// 英語老師使用時:
Student<Character> stu3 = new Student<Character>("張三", 'C');
Student<?>[] arr = new Student[3];
arr[0] = stu1;
arr[1] = stu2;
arr[2] = stu3;
StudentService.print(arr);
}
}
12.4.2 <? extends 上限>
例如:我們要宣告一個學生管理類,這個管理類要包含一個方法,找出學生陣列中成績最高的學生物件。
要求學生的成績的型別必須可比較大小,實現Comparable介面。
學生管理類:
class StudentService {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static Student<? extends Comparable> max(Student<? extends Comparable>[] arr){
Student<? extends Comparable> max = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i].getScore().compareTo(max.getScore())>0){
max = arr[i];
}
}
return max;
}
}
測試類
public class TestGeneric {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) {
Student<? extends Double>[] arr = new Student[3];
arr[0] = new Student<Double>("張三", 90.5);
arr[1] = new Student<Double>("李四", 80.5);
arr[2] = new Student<Double>("王五", 94.5);
Student<? extends Comparable> max = StudentService.max(arr);
System.out.println(max);
}
}
12.4.3 <? super 下限>
現在要宣告一個數組工具類,包含可以給任意物件陣列進行從小到大排序,只要你指定定製比較器物件,而且這個定製比較器物件可以是當前陣列元素型別自己或其父類的定製比較器物件
陣列工具類:
class MyArrays{
public static <T> void sort(T[] arr, Comparator<? super T> c){
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length-i; j++) {
if(c.compare(arr[j], arr[j+1])>0){
T temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
例如:有如下JavaBean
class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Person() {
super();
}
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;
}
@Override
public String toString() {
return "name=" + name + ", age=" + age;
}
}
class Student extends Person{
private int score;
public Student(String name, int age, int score) {
super(name, age);
this.score = score;
}
public Student() {
super();
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
return super.toString() + ",score=" + score;
}
}
12.4.4 使用型別萬用字元來指定型別引數的問題
<?>:不可變,因為<?>型別不確定,編譯時,任意型別都是錯 <? extends 上限>:因為<? extends 上限>的?可能是上限或上限的子類,即型別不確定,編譯按任意型別處理都是錯。 <? super 下限>:可以將值修改為下限或下限子類的物件,因為<? super 下限>?代表是下限或下限的父類,那麼設定為下限或下限子類的物件是安全的。 ```java public class TestGeneric { public static void main(String[] args) { Student<?> stu1 = new Student<>(); stu1.setScore(null);//除了null,無法設定為其他值
Student<? extends Number> stu2 = new Student<>();
stu2.setScore(null);//除了null,無法設定為其他值
Student<? super Number> stu3 = new Student<>();
stu3.setScore(56);//可以設定Number或其子類的物件
}
}
class Student{
private String name;
private T score;
public Student() {
super();
}
public Student(String name, T score) {
super();
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public T getScore() {
return score;
}
public void setScore(T score) {
this.score = score;
}
@Override
public String toString() {
return "姓名:" + name + ", 成績:" + score;
}
}