java筑基 - 泛型(一)

java泛型 一

    • 什么是泛型
    • 为什么要使用泛型
    • 通用类型
      • Response通用版本
      • 类型参数命名规则
      • 调用和实例化泛型类型
      • 菱形
      • 多种类型参数
      • 原始类型
      • 未检查的错误信息
    • 通用方法
    • 限定类型参数(重点)
      • 多重限定
      • 通用方法和限定类型参数
      • 泛型、继承和子类型*
      • 通用类和子类型
    • 通配符(重点)
      • 上界通配符(extend、 in)
      • 无界通配符
      • 下界通配符
      • 总结:通配符和子类型

如果本文满足不了大家对泛型的理解,可以自行参考官方文档

  • 英文文档:https://docs.oracle.com/javase/tutorial/java/generics/index.html

  • 中文文档:https://pingfangx.github.io/java-tutorials/java/generics/rawTypes.htm

什么是泛型

泛型(Generic),是java5之后出现的一个强大的功能。它提高了代码的类型安全性,可以在编译时检测更多的错误。

在平时的开发中,有些错误可能比较容易被发现。比如,在早期发现编译错误,我们可以使用编译器来帮我们找到问题所在。但是,有些运行时错误可能会带来更多的问题,它们更不容易被我们发现。

泛型的出现,可以说在一定程度上解决了大部分的问题,它可以在编译时做出更多的检测来保证代码的稳定性。

为什么要使用泛型

泛型可以在定义类、接口或方法时成为参数。通俗的讲,它与方法中声明的形参类似,泛型就是提供了一种使用不同输入重复使用相同代码的方法。区别在于形参输入的是具体的类型的值,而泛型传入的是任意一种类型。

使用泛型具有许多优点:

  • 在编译时进行更强的类型检测。Java编译器讲强类型检测应用于通用代码,如果代码违反类型安全,就会发生错误。相当于把错误由运行时提前到了编译时,更容易让开发者进行查找和修复。

  • 消除类型转换。如果不使用泛型,就要进行类型强制转换,提高了错误发生率。

    • 不带泛型:

      List list = new ArrayList();
      list.add("123");
      list.add(1);
      String s = (String) list.get(0);
      int i = (int) list.get(1);
      // 这样就会出现类型转换异常
      // String c = (String) list.get(1);
      

      可以使用各种类型的值往里面塞,使用的时候需要强转,一不小心就会出错.

    • 使用泛型:

      List<String> list2 = new ArrayList<>();
      list2.add("234");
      // 使用了泛型,就必须使用泛型类型,否则就会在编译器报错
      // list2.add(213);
      String ss = list2.get(0);
      

      这样指定了一种类型,当使用时,就不需要进行强制转换。通过使用泛型,我们可以实现对不同类型的集合进行工作,可以自定义实现类型安全且易于阅读的泛型算法。

通用类型

通用类型时通过类型进行参数化的通用类或接口。拿们比较熟悉的举例,在java4及以前,人们定义一个通用的类可能时这样的:

public class Response {

    private int code;
    private Object data;
    private String msg;

    public Response(int code, Object data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }
}

public static void main(String[] args) {
    List l = new ArrayList();
    l.add("123");
    l.add(1);
    
 	Response response = new Response(0,list,"success");  
    
    
    
    List list = (List) response.getData();
    String s = (String) list.get(0);
    int i = (int) list.get(1);
    
    // 取错了
    // String obj = response.getData();
    // ......
    // 或者这样
    // String i = (String) list.get(1);
}

Response方法可以接受或返回一个Object, 所以我们可以传递任何想要的东西进去。在编译时无法验证类的使用方式。以上例子展示了一个难以维护甚至让人想骂街的代码,如果不小心哪里错了,那肯定是运行时错误,这样查找和维护起来就非常麻烦了。

Response通用版本

通用类的定义格式如下:

class Name<T1,  T2, ... ,Tn> { /* ... */ }

在类型之后,类型参数部分,由<>分科。它指定了类型参数(也称为类型变量)T1, T2, … ,Tn

以此进行改进后,Response类变为:

public class Response<T> {

    private int code;
    private T data;
    private String msg;

    public Response(int code, T data, String msg)     {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }
}

我们把所有不确定类型Object,替换为泛型T。这个T就是类型变量。它可以被指定为任何非基本类型(比如:类类型、接口类型、数组类型、甚至可以是另一个类型变量)。也可以使用相同的技术应用于创建通用接口。

类型参数命名规则

上面讲的大家都很常见了。不过按照约定,类型参数也是由命名 规则的:

类型参数名称时单个大写字母。这与你已知的变量命名约定形成鲜明的对比,并且有充分的理由:没有该约定,将很难分别泛型变量与普通类之间的区别。

常用的类型参数名称:

  • E - Element (Java Collections ,在Framework层广泛使用)
  • K - 键值对 ,key
  • V - 键值对, value
  • N - Number
  • T - Type
  • 还有S,U,V等等,这里就不多做介绍了,有兴趣可以在官方文档上找找

调用和实例化泛型类型

如果使用Response类,就必须将T替换为某些具体的类型,比如Integer、List、Bean等:

Response<List> response = new Response<List>();
Response<Integer> response1 = new Response<>();

菱形

在 Java SE 7以及更高版本中,只要编译器可以从上下文推断出类型参数,就可以用一组空的类型参数(<>)替换调用类的构造函数所需的类型参数。这对尖括号被称为菱形。比如上个例子所见:

Response<Integer> response1 = new Response<>();

多种类型参数

上面已经提到,泛型类可以具有多个类型参数。例如,HashMap实现了Map接口:

public interface Map<K, V> {
    V get(Object key);
    V put(K key, V value);
	// ...
}

public public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
 	   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
}

创建Map时就可以这样:

Map<String, Integer> m1 = new HashMap<>();
m1.put("m", 1);
Map<String, String> m2 = new HashMap<>();
m2.put("m", "1");

通过new HashMap将K转换为String,将 V实例化为Integer。所以,HashMap的put方法的连个参数类型分别时String和Integer。这就是Java自动装箱,将String和int传递给类是有效的。

稍微复杂一点的,还可以通过参数化类型替换类型参数。比如:

Map<String, Response<List<String>>> map = new HashMap();
List<String> list = new ArrayList<>();
map.put("m", new Response(0, list, "msg"));

原始类型

原始类型就是没有任何类型参数的泛型类或接口的名称。(注意:如果时kotlin的话,这时不被允许的!)

还是拿Response举例:

class Response<T> {
	public void set(T t) {
		/* ... */
	}
}

要创建 一个Response的原始类型,就是省略实际的参数类型:

Response response = new Response();

所以Response就是原始类型。但是非泛型类不能称为原始类型。

因为在Java5之前,还没有泛型的概念,所以许多API类不是通用的,比如Collections,使用原始类型时,实际上会获得泛型的行为。为了向后兼容,所以才出现了原始类型,并且允许将参数化类型分配给其原始类型:

Response<String> res = new Response<>();
Response rawRes = res;

但是,如果时将原始类型分配给参数化类型,就会收到警告:

(warning: unchecked conversion)

Response res = new Response();
Response<Integer> intRes = res; // 警告

如果使用原始类型调用在响应的泛型类型中定义的泛型方法,也会收到警告:

(wraning: unchecked invocation to set(T))

Response<String> res = new Response<>();
res.setData("234");
Response rawRes = res;
rawRes.setData(123); // 警告

该警告表名原始类型会绕过通用类型检测,从而将不安全代码的捕获推迟到运行时。因此,应尽量避免使用原始类型。这也是为什么kotlin本身就不支持原始类型的原因,java也是迫不得已啊!

未检查的错误信息

(Unchecked Error Message)

如果将旧代码与通用代码混合时,可能会遇到以下的警告信息:

Note: Example.java uses unchecked or unsafe operation
Note: Recompile with -Xlint:unchecked for details.

比如旧的api使用了原始类型 :

public class Test { 
	public  static void main(String[] args) {
		Response<String> res =  createRes();
	}
	
	public static Response createRes() {
		return new Response();
	}
}

我们编译项目时可能会经常遇到 上面的警告,以上就是引发警告的原因之一。

术语“未检查”表示编译器没有足够的类型信息来执行确保类型安全所需的所有类型检查。尽管编译器会给出提示,但默认情况下禁用“未检查”警告。要查看所有“未检查”的警告,请使用 -Xlint:unchecked 重新编译。

使用 -Xlint:unchecked 重新编译前面的示例将显示以下附加信息:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning

可以使用@SuppressWarnings(“unchecked”) 注解来解除禁止未检查的警告。不过这种需要小心使用,否则也会有运行时错误的可能性,比如:

public static Response create() {
        Response res = new Response();
        res.setData(123);
        return res;
}

public static void main(String[] args) {
        Response<String> resas= create();
        System.out.println("data === " + resas.getData());
}

create()创建了一个通用类型,并赋值为int类型123, 在使用时使用了String类型接收,这时会出现警告但并不会报错,运行的时候会报错:ClassCastException,所以这种情况一定要加倍小心。

通用方法

通用方法时指引入自己类型参数的方法,类似于声明一个泛型方法,但是类型参数的范围仅限于声明它的方法 。允许使用静态和非静态的泛型方法,也允许使用 泛型类构造函数。

通用方法的语法包括类型参数列表,在尖括号内,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。Util类包含一个通用方法 compare ,该方法比较两个Pair对象:

public class CommonFun {

    public static void main(String[] args) {
        Pair<Integer, String> p1 = new Pair<>(1, "1");
        Pair<Integer, String> p2 = new Pair<>(1, "1");
        boolean same = Utils.compare(p1, p2);
        System.out.println("same == " + same);
    }

    static class Utils {
        static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
            return p1.getKey().equals(p2.getKey()) &&
                    p1.getValue().equals(p2.getValue());
        }
    }

    public static class Pair<K, V> {
        private K key;
        private V value;
        public Pair(K key, V value) {
            this.key = key;
            this.value = value;
        }
        public K getKey() { return key; }
        public void setKey(K key) { this.key = key; }
        public V getValue() { return value; }
        public void setValue(V value) { this.value = value; }
    }
}

如上main方法所示:boolean same = Utils.compare(p1, p2);

其完整语法为 :boolean same = Utils.compare(p1, p2);

通常,可以将其忽略,编译器将推断出所需的类型,此功能也是类型推断,使你可以在不指定尖括号之间的类型的情况下,将通用方法作为普通方法调用。

限定类型参数(重点)

通过修改通用方法类型以包含此限定类型参数:

public class BoundedTypeTest {
    public static void main(String[] args) {
        test("12123");
        test(123);
    }

    private static <U extends Number> void test(U u) {
        System.out.println("type:::" + 	
        u.getClass().getCanonicalName());
    }
}

由于test方法被限定为Number的子类,调用时仍然传入String,这时编译器将会报错:

Error:(3, 9) java: 无法将类 BoundedTypeTest中的方法 test应用到给定类型;
  需要: U
  找到: java.lang.String
  原因: 推断类型不符合上限
    推断: java.lang.String
    上限: java.lang.Number

除了限制可用于实例化泛型类型的类型之外,限定类型参数还允许你调用在范围中定义的方法:

public class NaturalNumber<T extends Integer> {
	private T n;
	public NaturalNumber(T n) { this.n = n; }
	public boolean isEven() {
		return n.intValue() % 2 == 0;
	} 
    // ...
}

isEven 方法通过n调用Integer类中定义的intValue方法

多重限定

上面的示例说明了 使用单个限定的类型参数,我们一个类型参数可以具有多个限定:

<T extends B1 & B2 & B3>

具有多个限定的类型变量是范围中列出的所有类型的子类型。如果范围之一是 类,那么必须先指定他:

private class MultipleType<T extends Response & A & B>   {

}

private interface A {

}

private interface B {

}

通用方法和限定类型参数

Generic Methods and Bounded Type Parameters

限定类型参数是实现通用算法的关键。如下:

    public static <T> int thanCount(T[] array, T ele) {
        int count = 0;
        for (T t : array) {
            if (t > ele) { // error
                count++;
            }
        }
        return count;
    }

这样将会编译报错,因为 > 符号 只能用于基本类型 ,所以不能使用>比较对象。如果解决这个问题,就要使用Comparable接口限定的类型参数:

public static void main(String[] args) {
    int count = thenCount(resArr, res);
    System.out.println("count === " + count);
}

interface Comparable<T> {
        public int compareTe(T t);
    }

static class A implements Comparable<A> {

        @Override
        public int compareTe(Integer integer) {
            return 0;
        }
    }

    public static <T extends Comparable<T>> int thenCount(T[] array, T ele) {
        int count = 0;
        for (T e : array) {
            if (e.compareTe(ele) > 0) {
                count++;
            }
        }
        return count;
    }

泛型、继承和子类型*

众所周知,只要类型兼容,就可以将一种类型的 对象分配给另一种类型的对象,比如:

Integer i = new Integer(1);
Object obj = i;

由于Integer属于Object,因此这样是允许被分配的,Integer也是Number的子类,所以下面也是OK的

Response<Number> res = new Response<>();
res.setData(new Integer(1));
res.setData(new Double(1.2));

泛型也是一样,我们 可以通过 执行 通用类型调用,将Number作为其类型参数传递,并且如果该参数是Number类型的子类型,也就是与Number类型兼容,那么就可以 正常执行。

但是重点来了, Response是不能转换成Response的,比如:

Response<Number> res = new Response<>();
Response<Integer> res1 = new Response<>();
res1 = res; // 不可强制转换
res = res1; // 不可强制转换

这里是重点,一定要注意,ResponseResponse相当于是两种类型,他们之间没有任何关系!

通用类和子类型

你可以通过扩展或实现来泛型通用类或接口。一个类或接口的类型参数与另一类或接口的类型参数的关系由 extends 和 implements 子句确定。
以 Collections 类为例, ArrayList 实现 List ,而 List 扩展 Collection 。因此, ArrayList是List 的子类型,而 List 是 Collection 的子类型。只要你不改变类型参数,子类型关系就保留在之间。

PayloadList
PayloadList
PayloadList

以上参数化类型就是List的子类型。

通配符(重点)

在通用代码中, 问号(?)就是通配符,表示未知类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;也可能作为返回类型。通配符从不用作泛型方法调用,翻新类实例创建或超类型的类型参数。

下面我们将详细的讨论通配符:上界、下界通配符和通配符捕获

上界通配符(extend、 in)

我们可以使用上界通配符来放宽对变量的限制。比如我们要编写一个适用于List,List,和List的方法,可以使用上限 通配符来实现。

在java中,要声明上界通配符,使用(? extend Type)格式,在kotlin中则使用(in Type)。

比如,要求一个列表中的数字之和:

public static void main(String[] args) {
        List<? extends Number> l1 = Arrays.asList(1, 2, 3.4, 5f);
        System.out.println(sum(l1)); // 11.4

        List<Integer> l2 = Arrays.asList(1, 2, 3, 4);
        System.out.println(sum(l2)); // 10.0

        List<Double> l3 = Arrays.asList(1.1, 2.5, 3.0, 4.6);
        System.out.println(sum(l3)); // 11.2
    }


    static double sum(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list) {
            s += n.doubleValue();
        }
        return s;
    }

以上结果将会正常打印。

这里有一点,上界通配符指定的List可以读,但是不能写入,比如

static double sum(List<? extends Number> list) {
		// 这样会报错 list.add(123);
       
        return 0.0
}

无界通配符

无界通配符使用?来指定,比如List。这就是未知类型的列表。在某些情况下,无界通配符是一种有用的方法。

  • 如果你正在编写一个可以使用Object类中提供的功能实现的方法
  • 当代吗使用 通用类中不依赖于类型参数的方法时。例如list.size()或list.clear()。我们经常看到Class的用法,实际上,Class之所以经常这么用,是因为Class中的大部分方法都不依赖于T

结合上面说的,原始类相同,泛型不同的两个类是不可以强转的。比如下面的方法:

public static void main(String[] args) {
        List<Object> list = Arrays.asList(1, 2, 3);
        printList(list);

        List<Integer> l2 = Arrays.asList(1, 2, 3);
        // 这样做不行 printList(l2);
    }

public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
}

我们传一个Object类型的List进去是没有问题的,可是当我们传递Integer类型的List编译器就会报错;因为它们并不是同一个类型。那么如果要编写通用的printList方法,就要使用List:

public static void main(String[] args) {
        List<Object> list = Arrays.asList(1, 2, 3);
        printList(list);

        List<Integer> l2 = Arrays.asList(1, 2, 3);
        printList(l2);
    }

public static void printList(List<?> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
}

注意:示例中均使用 Arrays.asList 方法。此静态工厂方法将转换指定的数组并返回固定大小的列表。

下界通配符

上界通配符将未知类型限制为特定类型或该类型的子类型。同理下界通配符则相反,它将未知类型限制为特定类型 或该类型的超类型。(注意,通配符可以指定上界或下界,但不能两者同时指定)

假设你要编写一个将 Integer 对象放入列表的方法。为了最大程度地提高灵活性,你希望该方法可用于 List , List 和 List (可以容纳 Integer 值的任何内容)。

要编写对 Integer 的列表和 Integer 的超类型(如 Integer 、 Number 和 Object )的方法,你可以指定 List 。术语 List 比 List 的限制性更强,因为前者只匹配 Integer 类型的列表,而后者则匹配作为 Integer 的超类型的任何类型的列表。

以下代码将数字1到10添加到列表的末尾:

public static void main(String[] args) {
    List<Double> li1 = new ArrayList<>();
    //error addNumber(li1);
    
    List<Number> li2 = new ArrayList<>();
    addNumber(li2); // ok
    
    List<Integer> li3 = new ArrayList<>();
    addNumber(li3); // ok
    
    List<Object> li4 = new ArrayList<>();
    addNumber(li4); // ok
    
}

private static void addNumber(List<? super Integer> li) {
    for (int i = 0; i < 10; i++) {
        li.add(i);
    }

    // 这样不行 int a = li.get(2);
}

这里也要注意一下,下界通配符可以往里面添加元素,但是并不可读。恰恰与上界通配符相反

总结:通配符和子类型

如泛型、继承和子类型中所讲,泛型或接口不仅仅因为它们之间存在关系而相关。但我们可以用通配符在通用类或接口之间创建关系。

以下给定两个普通类

public static void main(String[] args) {
	B b = new B();
	A a = b;
}

static class A {}
static class B extends A {}

这样是没问题的,B是A的子类型,但是此规则不适用于通用类型:

List<B> lb = new ArrayList<>();
List<A> la = lb; // error

这样将会编译失败。虽然,B是A的子类型,但是ListList它们之间并没有关系,它们之间公共的父级是List。为了在它们之间创建关联,就可以使用上界通配符:

List<B> lb = new ArrayList<>();
List<? extend A> la = lb; // ok

好了,关于泛型的理解今天就先到这,下一篇重点讲解一下通配符的限制和类型擦除机制。今天所用的Demo我已经上传到Github,如果有需要的小伙伴可以对照着理解,这样会比较容易一些。

传送门

你可能感兴趣的:(Java基础)