参考文献:https://pingfangx.github.io/java-tutorials/java/generics/types.html
Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了 编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数。 泛型不存在于JVM虚拟机。
通俗点讲,就是将类型参数化,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这样提高了代码的类型安全性,使你在编译时可以检测到更多错误。
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);//需要强制转换
list.add(1);
s =(String) list.get(1); //在编译时期没有任何错误提示 在运行时期会报错
使用泛型的代码如下:
List list = new ArrayList();
list.add("hello");
String s = list.get(0); //不需要进行强制转换
list.add(1);
s =list.get(0); //会在编译期就报错 Required type:String Provided:int
通过上面官方例子,我们不难发现,与非泛型代码相比,使用泛型的代码具有以下优点:
1,在编译时进行更强的类型检查。Java编译器将强类型检查应用于通用代码,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误容易,后者可能很难找到。
2,消除类型转换。当使用泛型重写时,代码不需要强制转换。
3,使程序员能够实现通用算法。通过使用泛型,程序员可以实现对不同类型的集合进行工作,可以自定义并且类型安全且易于阅读的泛型算法。
泛型类是通过类型进行参数化的通用类。
泛型类的定义格式如下:
class 类名 {
private 泛型标识 变量名;
}
//例如:
class Student {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
上面T可以随便定义,只要是在<>中,你甚至可以定义为dnadnandn都行,注意:尽量不要定义为关键字,以免引起冲突。
注意:类型参数与类型变量的区别:
Student
Student
常用的类型参数名称是:
T - Type类型 最常用
E - 元素或Exception异常
K - Key
V - Value
N - 数字
S,U,V 等:第二,第三,第四个类型
下面看一个简单的例子
泛型类:
public class MyList {
private List list;
public void add(T t){
list.add(t);
}
public T get(int i){
return list.get(i);
}
public int size(){
return list.size();
}
}
不使用泛型的类:
public class MyList1 {
private List
在使用时:
//泛型类的使用
MyList myList =new MyList<>();
myList.add("aaa");
//myList.add(1); 在编译时就报错
for (int i = 0; i < myList.size(); i++) {
String s1 = myList.get(i);
System.out.println("s1="+s1);
}
//非泛型类的使用
MyList1 myList1 =new MyList1();
myList1.add(11);//不会报错
myList1.add("111");
for (int i = 0; i < myList1.size(); i++) {
String s1 = (String) myList1.get(i);//1,需要强转 2,在运行时会报错
System.out.println("s1="+s1);
}
泛型类还可以传入多个泛型参数,下面看一个 两个参数的泛型类:
public class People {
private K name;
private V achievement;
public People(K name, V achievement) {
this.name = name;
this.achievement = achievement;
}
public void setName(K name) {
this.name = name;
}
public void setAchievement(V achievement) {
this.achievement = achievement;
}
public K getName() {
return name;
}
public V getAchievement() {
return achievement;
}
}
People p1 =new People<>("张三",98);
People p2 =new People<>("赵武",99);
// People p3 =new People<>("赵武","100");//编译阶段报错
需要注意的是泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数,例如:
public class Test {
private static T name;//编译时报错'com.yuanzhen.Test.this' cannot be referenced from a static context
public static T getName(){//编译时报错'com.yuanzhen.Test.this' cannot be referenced from a static context
return name;
}
}
原因是泛型在对象创建时才知道是什么类型,但是静态方法属于类,调用getName方法实际调用的Test类的方法,而类在编译阶段就存在了,所以虚拟机根本不知道方法中引用的泛型是什么类型
初始化时:对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等,所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西
泛型接口的定义和泛型类的定义一样,区别就在于一个是接口需要实现各个接口方法,一个是类,需要实现对应的抽象方法。
泛型接口的定义格式如下:
public interface 接口名<类型参数> {
...
}
//例如
public interface IPeople {
T getName();
void setName(T t);
}
泛型的具体使用方式:
①直接在实现类中指定泛型的具体类型
public class Student implements IPeople{
@Override
public String getName() {
return null;
}
@Override
public void setName(String string) {
}
}
②在实现类中继续使用泛型,在实例化实现类对象的时候指定泛型的具体类型
public class Teacher implements IPeople{
@Override
public T getName() {
return null;
}
@Override
public void setName(T t) {
}
}
Teacher teacher =new Teacher<>();
teacher.setName("张三");
③在接口继承接口中指定泛型的具体类型。
public interface ITeacher extends IPeople{
}
public <类型参数1,类型参数2...> 返回类型 方法名(类型参数1 变量名1,类型参数2 变量名2 ...) {
...
}
//例如:
public class Doctor {
/**
* 泛型方法
*/
public void doWork(T t){
}
}
注意:只有在方法声明中声明了<>的方法才是泛型方法
下面看个例子:
public class Doctor {
private T name;
/*
* 不是泛型方法 只是普通方法
* */
public Doctor(T name) {
this.name = name;
}
/*
* 不是泛型方法 只是普通方法
* */
public void setName(T name) {
this.name = name;
}
/*
* 不是泛型方法 只是普通方法
* */
public T getName() {
return name;
}
/*
* 泛型方法 因为它定义了自己的
* */
public void doWork(T t){
}
}
调用泛型方法:
Doctor doctor =new Doctor<>("张三");
doctor.doWork("看病");//调用泛型方法的完整语法
doctor.doWork("看病");//因为类型推断,所以可以省略
类型推断:上文中可以看出,因为类型推断,可以省略<>,那么什么是类型推断呢?
类型推断(Type Inference)是指 Java 编译器能查看每个方法的调用和相应声明,以确定调用合适的类型参数(Type Argument)或参数。推断算法决定参数的类型,如果可用,则指定被赋值的类型或返回的类型。最后,推断算法试图在一起工作的所有参数中找到最具体的类型。
引入类型推断的泛型方法,能够让你像调用普通方法一样调用泛型方法,而不需要在尖括号中指定类型。
//单个限定
//多重限定
举例如下:
public class Teacher{
private T age;
public T getAge() {
return age;
}
public void setAge(T t) {
}
}
Teacher teacher1 =new Teacher<>();//报错 因为已经限定了类型必须是Number及其子类
teacher1.setAge("张三");
//正确用法
Teacher teacher2 =new Teacher<>();
teacher2.setAge(20);
格式如下:
extends T>
//例如:
extends Foo>
//其中,Foo可以是任何类型,匹配Foo和Foo的任何子类型
下面来看一个具体的使用案例:
public class Fruit {
@Override
public String toString() {
return "水果";
}
}
public class Apple extends Fruit {
@Override
public String toString() {
return "苹果";
}
}
ArrayList fruits =new ArrayList();//这样会在编译期报错
这样为什么会报错呢?Java不是可以将子类对象赋值给一个父类对象的引用吗?
下来再来看一下这段代码:
public class Banana extends Fruit {
@Override
public String toString() {
return "香蕉";
}
}
ArrayList apples =new ArrayList();
Apple apple =new Apple();
apples.add(apple);
//----------------上面的代码不会报错 是正常逻辑------------
ArrayList fruits =apples;//假如这行代码在编译期不报错的话
fruits.add(new Banana());
Apple a =fruits.get(1);//这行代码在运行期间就会报类型转换异常的错误
所以泛型是不支持这种向上转型的。
但是如果我们一定要这么写呢?也不是不可以,用通配符就可以做到
ArrayList extends Fruit> fruits =new ArrayList();//编译期不会报错
Apple apple =new Apple();
fruits.add(apple);//但是在添加的时候会在编译期报错
大家想一想为什么会在添加的时候会在编译期报错呢?
因为如果放开的话,我还是同样的可以添加香蕉,苹果等子类,这样在运行期间就可能会出现更大的错误,所以编译器直接就不让你添加了,这样就不会有问题了。
但是这样做有什么意义呢?
当你只想让用户往外取值,不想让用户进行写入时, extends Fruit>的意义就体现出来了,简而言之就是只能读取不能写入
格式如下:
super T>
//例如:
supper Zoo>
//其中,Zoo可以是任何类型,匹配Zoo和Zoo的任何超类
下面来看一个例子:
ArrayList fruits =new ArrayList();//会在编译期报错
Apple apple =new Apple();
fruits.add(apple);
为什么会在编译期报错呢?同上限通配符一样的道理,泛型也不支持这种转型
那么需要怎么做呢?请看下面:
ArrayList super Apple> fruits =new ArrayList();//不报错 正常运行
Apple apple =new Apple();
fruits.add(apple);
Object object = fruits.get(0);
使用下限通配符为什么可以添加呢? 因为往里面添加的都是apple的父类,其归根结底,都会用一个最终的父类表示,所以不会有问题。但是往外读的时候,我不知道是苹果还是水果,所以不建议读取,虽然可以用Object接收。
简而言之,与上限通配符相反,下线通配符只能写入,不能读取
在开发中,我们应该在什么时候使用上限通配符和下限通配符呢?
通配符准则:
使用上限通配符定义输入变量,使用 extends 关键字。
使用下限通配符定义输出变量,使用 super 关键字。
如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
下面来看几个例子:
public class Node {
private T data;
private Node next;
public Node(T data, Node next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
}
public class Node> {
private T data;
private Node next;
public Node(T data, Node next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
}
请看下面例子:
class Pair {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
}
创建对对象时,不能用基本类型替换类型参数K或V:
Pair p = new Pair<>(8, 'a');//编译错误
你只能将非基本类型替换为类型参数K和V:
Pair p = new Pair<>(8, 'a');
例如,以下代码会导致编译时错误:
public static void append(List list) {
E elem = new E(); //编译期错误
list.add(elem);
}
解决方法是,可以通过反射创建类型参数的对象:
public static void append(List list, Class cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
类的静态字段是该类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段
public class MobileDevice {
private static T os;
}
public static void rtti(List list) {
if (list instanceof ArrayList) { // 编译器错误
// ...
}
}
List[] arrayOfLists = new List[2];//编译期报错
class MathException extends Exception { /* ... */ } // 编译期错误
class QueueFullException extends Throwable { /* ... */ }// 编译器错误
public static void execute(List jobs) {
try {
for (J job : jobs)
} catch (T e) { //编译期错误
}
}
class Parser {
public void parse(File file) throws T { // OK
}
}
public class Example {
public void print(Set strSet) { }
public void print(Set intSet) { }
}
那么在了解了泛型的知识之后,我们在安卓开发中,到底用到了哪些泛型呢?
最常用的地方,就是解析http请求下来的json数据。因为http请求下来的数据,我们不知道需要转换成什么类型,只有在调用的地方才知道,所以我们采用泛型。
先定义一个回调接口:
public interface CallBack {
void onError(Exception e);
void onSuccess(T t);
}
在实际请求中传入匿名内部类:
OmniHttp.get(getHOST() + "/app-wn/login")
.syncRequest(true)
.execute(new CallBack() {
@Override
public void onError(ApiException e) {
}
@Override
public void onSuccess(LoginBean loginBean) {
}
});
这只是简单的写了一个小例子,可以尝试在自己写一下
/**
* FindViewById的源码,泛型封装,减少强转代码
*/
public T findViewById(@IdRes int id) {
return getDelegate().findViewById(id);
}
//使用前
ImageView item = (ImageView)findViewById(R.id.item);
//使用后
ImageView item =findViewById(R.id.item);
有时间补充
有时间补充
花了一天时间整理了下泛型的相关知识点,在android中的应用后续还会继续补充。