java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java SE泛型

Java SE之了解泛型

作者:程序猿教你打篮球

这篇文章主要介绍了Java SE之了解泛型,文章内容详细,简单易懂,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

如何创建可以存放各种类型的数组?

根据JavaSE的语法知识储备,如果现在让你们创建如标题一样的数组,你会怎么创建呢?

答案是:使用 Object 类来定义数组,因为 Object 是所有类的父类, 可以接收任意子类对象,也即实现了向上转型,于是我们就写出了这样的代码:

private Object[] array = new Object[3];

那么这种方法可取吗?

显然是可取的,但只是使用起来会很不方便,具体不方便在哪,我们接着往后看,在这里我们要写一个类,里面提供了获取array指定下标的数据,和设置array指定下标的数据,于是写出了这样的代码:

public class DrawForth {
    private Object[] array = new Object[3];
 
    public void setPosArray(int pos, Object o) {
        this.array[pos] = o;
    }
    public Object getPosValue(int pos) {
        return this.array[pos];
    }
}

代码到这里仍然是正确的,那我们就要去使用这个类,也就是在main方法中用这个类实例对象,去操作里面的数组,所以main方法的代码就是这个样子:

public static void main(String[] args) {
        DrawForth draw = new DrawForth();
        draw.setPosArray(0, 123);
        draw.setPosArray(1, "hello");
        draw.setPosArray(2, 12.5);
 
        int a = (int)draw.getPosValue(0);
        String str = (String)draw.getPosValue(1);
        double d = (double)draw.getPosValue(1);
    }

看到这里,你是不是就发现这样做很不方便呢?

当我们往数组里面设置数据的时候开心了,想设置成什么类型就是什么类型,但是!当我们要获取对应位置的元素就麻烦了,我们必须知道他是什么类型,然后进行强制类型转换才能接收,(返回是Object类型所以需要强转),难道往后每次取数据的时候我还得看一看是什么类型吗?

泛型的概念

浅聊泛型

泛型是在JDK1.5引入的新的语法,通过上面的例子,由此我们就引出了泛型,泛型简单来说就是把类型当成参数传递,指定当前容器,你想持有什么类型的对象,你就传什么类型过去,让编译器去做类型检查!

从而实现类型参数化(不能是基本数据类型)

泛型的简单语法

class Test1<类型形参列表> {
 
}
class Test2<类型形参1, 类型形参2, ...> {
 
}

类型形参列表的命名规范

类名后面的 <类型形参列表> 这是一个占位符,表示当前类是一个泛型类,形参列表里面如何写?

通常用一个大写字母表示,当然,你也可以怎么开心怎么来,但是小心办公室谈话警告哈(dog),这里有几个常用的名称:

使用泛型知识创建数组

这里就来修改一下刚开始的代码,使用到泛型的知识,那么我们就可以这样修改:

public class DrawForth<T> {
    //private T[] array = new T[3]; error
    private T[] array = (T[])new Object[3];
 
    public void setPosArray(int pos, T o) {
        this.array[pos] = o;
    }
 
    public T getPosValue(int pos) {
        return this.array[pos];
    }
 
    public static void main(String[] args) {
        DrawForth<Integer> draw = new DrawForth<>();
        draw.setPosArray(0, 123);
        //draw.setPosArray(1, "hello"); error
        //draw.setPosArray(2, 12.5); error
        draw.setPosArray(1, 1234);
        draw.setPosArray(2, 12345);
 
        int a = draw.getPosValue(0);
        int b = draw.getPosValue(1);
        int c = draw.getPosValue(2);
    }
}

如上修改之后的代码,我们可以得到以下知识点:

什么是裸类型

裸类型就是指在实例化泛型类对象的时候,没有传类型实参,比如下面的代码就是一个裸类型:

DrawForth draw = new DrawForth();

我现在可以告诉你,这样做编译完全正常,但我们不要去使用裸类型,因为这是为了兼容老版本的 API 保留的机制,毕竟泛型是 Java1.5 新增的语法。

泛型是如何编译的?

泛型的擦除机制

如果我们要看泛型是如何编译的,可以通过命令 javap -c 字节码文件 来进行查看:

如上代码是 2.4 段落中的代码,奇怪,明明传的实参是 Integer 类型,最后所有的 T 却变成了 Object 类型,这就是擦除机制

所以在Java中,泛型机制是在编译级别实现的,运行期间不会包含任何泛型信息。

提示:类型擦除,不一定是把 T 变成 Object(泛型的上界会提到)

再谈为什么不能实例化泛型数组?

知道了擦除机制后,那么 T[] array = new T[3]; 是不对的,编译的时候,替换为Object,不是相当于:Object[] array = new Object[3]吗?

在Java中,数组是一个很特殊的类型,数组是在运行时存储和检查类型信息, 泛型则是在编译时检查类型错误。

而且Java设定擦除机制就只针对变量的类型和返回值的类型,所以在编译时候压根不会擦除 new T[3]; 这个 T ,所以自然编译就会报错!

我们前面通过强制类型转换的方式创建了泛型数组,说过那样写并不好,正确的方式是通过反射创建指定类型的数组,由于现在没学习到反射,这里先放着就行。

什么是泛型的上界?

有了擦除机制的学习,泛型在运行时都会被擦除成 Object 但是并不是所有的都是这样,泛型的上界就是对泛型类传入的类型变量做一定的约束,可以通过类型边界来进行约束。

语法:

class 泛型类名称<类型形参 extends 类型边界> {
    //...code
}

这里我们来举两个例子:

例1:

这里简单分析一下,Student 继承了 Person 类,而 Teacher 没有继承 Person 类,接着 Test 类给定了泛型的上界, 那么 Test 类中 <> 里面是什么意思呢?

表示只接收 Person 或 Person 的子类作为 T 的类型实参。

通过 main 方法中的例子也可也看出,类型传参只能传 Person 或 Person 的子类。

例2:

还是简单分析一下,Student 类实现了 Comparable 接口,而 Teacher 类并没有实现, 接着 Test 类给定了泛型的上界, 那么 Test 类中 <> 里面是什么意思呢?

表示 T 接收的类型必须是实现 Comparable 这个接口的!

通过 main 方法中的例子也可也看出,类型传参只能传实现了 Comparable 接口的类 。

注意:如果泛型类没有指定边界,则可以默认视为 T extends Object。

再谈擦除机制

如果给泛型设置了上界,则会擦除到边界处,也就不会擦除成 Object!

class Person {}
 
class Student extends Person {}
 
public class Main<T extends Person> {
    T array[] = (T[])new Object[10];
    public static void main(String[] args) {
        Main<Student> main = new Main<>();
    }
}

这里 Main 方法中设定了泛型的上界,传的类型实参必须是Person的子类,所以编译时会不会被擦除成 Person呢?下面我们查看一下对应的字节码文件: 

显而易见,确实被擦除成了泛型的上界! 

包装类的知识

基本数据类型和包装类

在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了 一个包装类型。

装箱和拆箱

装箱和拆箱也可也被称为装包和拆包。

装箱:将一个基本数据类型值放入对象的某个属性中。

拆箱:将一个包装类型中的值取出放到一个基本数据类型中。

这里我们举例来更清楚的认识装箱和拆箱:

public class Test {
    public static void main(String[] args) {
        int a = 10;
        Integer integer1 = new Integer(a); //手动装箱
        Integer integer2 = Integer.valueOf(100); //手动装箱
 
        int b = integer1.intValue(); //手动拆箱
    }
}

自动装箱和拆箱

由上面的例子我们可以看出,手动装箱和拆箱会带来不少的代码量,为了减少开发者的负担,Java中提供了自动转换机制,比如:

public class Test {
    public static void main(String[] args) {
        Integer integer = 100; //自动装箱
        int a = integer; //自动拆箱
    }
}

一道面试题

以下代码输出什么? 

public class Test {
 
    public static void main(String[] args) {
        Integer a1 = 100;
        Integer a2 = 100;
        System.out.println(a1 == a2);
        Integer a3 = 200;
        Integer a4 = 200;
        System.out.println(a3 == a4);
    }
}

结果是:true false 

为什么是这样的答案?这里我们去看一下对应的字节码文件再分析:

通过观察字节码文件,我们可以看到,在自动装箱的过程中,调用了 Integer.valueOf 方法,那么我们就去看一看 valueOf 方法中做了一件什么事:

通过查看源码,我们也能看出此方法将始终缓存 -128到127范围内的值, 通过查看对应的 low 和 high 值也可也发现 low为 -128,high为127,cache 是一个缓存数组。

接着我们来阅读下这段代码的操作,如果传入的值是介于 -128和127 之间,则直接返回缓存数组对应下标的值,比如传入的值是 -127 也就返回 chache[-127+(-(-128))],也即1下标位置的值!

如果超出了 -128到127 的范围则是新 new 一个对象返回,只要是 new 就一定是一个新对象,地址也是唯一的。

而且引用类型用 == 比较,比较的是引用的对象的地址,看完上面的介绍,你能弄明白为什么输出 true 和 false 吗?

泛型方法

定义泛型方法的语法:

方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {

        //...code

}  

普通泛型方法

这里我们就举一个很简单的例子:

public class Test {
    public <T> T getValue(T value) {
        return value;
    }
    public static void main(String[] args) {
        Test test = new Test();
        int ret = test.<Integer>getValue(150); //不使用类型推导
        System.out.println(ret);
 
        double d = test.getValue(12.5); //使用类型推导
        System.out.println(d);
    }
}

这就是泛型方法,这里面有个关键词,类型推导,什么是类型推导呢?

类型推导就是编译器会根据你传参的数据,自动推断出你要传递的类型实参,你也可以不使用类型推导,他们的效果都是一样的。

静态泛型方法

既然有普通泛型方法,同理,也有静态的泛型方法,也就是在修饰符后面加上 static,静态泛型方法跟普通静态方法一样,都是通过类名访问,不依赖于对象:

public class Test {
    public static<T> T getValue(T value) {
        return value;
    }
    public static void main(String[] args) {
        int ret = Test.<Integer>getValue(150); //不使用类型推导
        System.out.println(ret);
 
        double d = getValue(12.5); //使用类型推导(静态方法可以直接访问同类中静态方法,可以不借助类名)
        System.out.println(d);
    }
}

通配符 

引出通配符 

我们先来看这样的一段代码:

class Message<T> {
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}
public class TestDemo {
    public static void fun(Message<String> temp){
        System.out.println(temp.getMessage());
    }
    public static void main(String[] args) {
        Message<String> message = new Message<>();
        message.setMessage("欢迎来到篮球哥的博客!");
        fun(message);
    }
}

如果你仔细观察,TestDemo 类中的 fun 方法是有局限性的,他的形参就限制了传过来的 Missage类的类型必须是String,也就是说,形参能接收的对象的类型参数必须是String类型。

所以如果我们 new Missage对象时,类型实参传的是 Integer 呢?fun方法就会报错:

所以为了解决以上的问题,就有了通配符的概念!

认识通配符

泛型T是确定的类型,一旦传类型了,就定下来了,而通配符的出现,就会使得更灵活,或者说更不确定,就好像他是一个垃圾箱,可以接收所有的泛型类型,但又不能让用户随意更改!

通配符:? 

现在我们就把上面的代码更改一下,运用上通配符:

public class TestDemo {
    public static void fun(Message<?> temp){
        System.out.println(temp.getMessage());
    }
    public static void main(String[] args) {
        Message<Integer> message1 = new Message<>();
        message1.setMessage(123);
        fun(message1);
        Message<String> message2 = new Message<>();
        message2.setMessage("欢迎来到篮球哥的博客!");
        fun(message2);
    }
}

这样我们的代码就不会出错,但是,你不能通过 fun 方法去修改你传递对象的内容,为什么呢?

站在 fun 的角度,他使用了 接收可以任意泛型类,所以他不能确定自己接收了什么对象的!也就无法对对象的值进行更改! 

这样代码还是不够好,如果真的什么泛型类都能接收,那不是乱套了,所以在此基础上,又增加了通配符的上界和下界!

通配符的上界

语法:<? extends 上界>   例如:<? extends Person>

表示只能接收的实参类型是 Person 或者 Person的子类

图例:

这里我们写一段伪代码,更改上面用例的方法:

public static void fun(Message<? extends Person> temp){
        //temp.setMessage(new Student()); //仍然无法修改!
        //temp.setMessage(new Person()); //仍然无法修改!
        Person person = temp.getMessage();
        System.out.println(person);
}

为什么还是不能修改对象的属性呢?

因为 temp 接收的是 Person 或 Person的子类,此时接收的是哪个子类无法确定,也就无法设置对象的属性。

因为我们知道只能接收 Person以及他的子类,所以我们就可以拿 Person 类型来接收 getMessage 的对象,因为 Person是他们的父类,获取的是子类对象就可以实现向上转型,是安全的。

总结: 通配符的上界,不能进行写入数据,只能进行读取数据。

通配符的下界

语法:<? extends 下界>   例如:<? super Person> 

表示只能接收的实参类型是 Person 或者 Person的父类

图例: 

这里我们写一段伪代码,更改上面用例的方法:

public static void fun(Message<? super Person> temp){
        temp.setMessage(new Student()); //可以修改,因为添加的是他的子类
        temp.setMessage(new Person()); //可以修改,因为添加的是他本身
        //Person person = temp.getMessage(); // 不能接收,不知道获取的是哪个父类
        System.out.println(temp.getMessage()); //只能输出
}

为啥下界就可以设置对象的属性呢?

因为只能接收本身以及父类的类型,所以我们可以setMessage 传子类对象,但是不能传递父类,因为修改成子类对象是向上转型是安全的,如果 setMessaget 传父类对象的话就是向下转型则不安全!

为啥不能 getMessage呢?因为你不知道形参接收的类型是哪个父类,只能去输出内容!

总结:通配符的下界,不能进行读取数据,只能写入数据。 

以上就是Java SE之了解泛型的详细内容,更多关于Java SE泛型的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文