《Java 编程的逻辑》笔记——第8章 泛型

声明:

本博客是本人在学习《Java 编程的逻辑》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

开头语

之前章节中我们多次提到过泛型这个概念,从本节开始,我们就来详细讨论 Java 中的泛型,虽然泛型的基本思维和概念是比较简单的,但它有一些非常令人费解的语法、细节、以及局限性,内容比较多。

后续章节我们会介绍各种容器类,容器类可以说是日常程序开发中天天用到的,没有容器类,难以想象能开发什么真正有用的程序。而容器类是基于泛型的,不理解泛型,我们就难以深刻理解容器类。那泛型到底是什么呢?

8.1 基本概念和原理

8.1.1 一个简单泛型类

我们通过一个简单的例子来说明泛型类的基本概念、实现原理和好处。

8.1.1.1 基本概念

我们直接来看代码:

public class Pair<T> {
   

    T first;
    T second;
    
    public Pair(T first, T second){
   
        this.first = first;
        this.second = second;
    }
    
    public T getFirst() {
   
        return first;
    }
    
    public T getSecond() {
   
        return second;
    }
}

Pair 就是一个泛型类,与普通类的区别,体现在:

  1. 类名后面多了一个 <T>
  2. first 和 second 的类型都是 T

T 是什么呢?T 表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入

怎么用这个泛型类,并传递类型参数呢?看代码:

Pair<Integer> minmax = new Pair<Integer>(1,100);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();

Pair<Integer>,这里 Integer 就是传递的实际类型参数。

Pair 类的代码和它处理的数据类型不是绑定的,具体类型可以变化。上面是 Integer,也可以是 String,比如:

Pair<String> kv = new Pair<String>("name","老马");

类型参数可以有多个,Pair 类中的 first 和 second 可以是不同的类型,多个类型之间以逗号分隔,来看改进后的 Pair 类定义:

public class Pair<U, V> {
   

    U first;
    V second;
    
    public Pair(U first, V second){
   
        this.first = first;
        this.second = second;
    }
    
    public U getFirst() {
   
        return first;
    }

    public V getSecond() {
   
        return second;
    }
}

可以这样使用:

Pair<String,Integer> pair = new Pair<String,Integer>("老马",100);

<String,Integer> 既出现在了声明变量时,也出现在了 new 后面,比较啰嗦,Java 支持省略后面的类型参数,可以这样:

Pair<String,Integer> pair = new Pair<>("老马",100);

8.1.1.2 基本原理

泛型类型参数到底是什么呢?为什么一定要定义类型参数呢?定义普通类,直接使用 Object 不就行了吗?比如,Pair 类可以写为:

public class Pair {
   

    Object first;
    Object second;
    
    public Pair(Object first, Object second){
   
        this.first = first;
        this.second = second;
    }
    
    public Object getFirst() {
   
        return first;
    }
    
    public Object getSecond() {
   
        return second;
    }
}    

使用 Pair 的代码可以为:

Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();
Integer max = (Integer)minmax.getSecond();

Pair kv = new Pair("name","老马");
String key = (String)kv.getFirst();
String value = (String)kv.getSecond();

这样是可以的。实际上,Java 泛型的内部原理就是这样的。

我们知道,Java 有 Java 编译器和 Java 虚拟机,编译器将 Java 源代码转换为 .class 文件,虚拟机加载并运行 .class 文件。对于泛型类,Java 编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通 Pair 类代码及其使用代码一样,将类型参数 T 擦除,替换为 Object,插入必要的强制类型转换。Java 虚拟机实际执行的时候,它是不知道泛型这回事的,它只知道普通的类及代码

再强调一下,Java 泛型是通过擦除实现的,类定义中的类型参数如 T 会被替换为 Object,在程序运行过程中,不知道泛型的实际类型参数,比如 Pair<Integer>,运行中只知道 Pair,而不知道 Integer,认识到这一点是非常重要的,它有助于我们理解 Java 泛型的很多限制。

Java 为什么要这么设计呢?泛型是 Java 1.5 以后才支持的,这么设计是为了兼容性而不得已的一个选择。

8.1.1.3 泛型的好处

既然只使用普通类和 Object 就是可以的,而且泛型最后也转换为了普通类,那为什么还要用泛型呢?或者说,泛型到底有什么好处呢?

主要有两个好处:

  • 更好的安全性
  • 更好的可读性

语言和程序设计的一个重要目标是将 bug 尽量消灭在摇篮里,能消灭在写代码的时候,就不要等到代码写完,程序运行的时候。

只使用 Object,代码写错的时候,开发环境和编译器不能帮我们发现问题,看代码:

Pair pair = new Pair("老马",1);
Integer id = (Integer)pair.getFirst();
String name = (String)pair.getSecond();

看出问题了吗?写代码时,不小心,类型弄错了,不过,代码编译时是没有任何问题的,但运行时程序抛出了类型转换异常 ClassCastException

如果使用泛型,则不可能犯这个错误,如果这么写代码:

Pair<String,Integer> pair = new Pair<>("老马",1);
Integer id = pair.getFirst();
String name = pair.getSecond();

开发环境如 Eclipse 会提示你类型错误,即使没有好的开发环境,编译时,Java 编译器也会提示你。这称之为类型安全,也就是说,通过使用泛型,开发环境和编译器能确保你不会用错类型,为你的程序多设置一道安全防护网

使用泛型,还可以省去繁琐的强制类型转换,再加上明确的类型信息,代码可读性也会更好。

8.1.2 容器类

泛型类最常见的用途是作为容器类,所谓容器类,简单的说,就是容纳并管理多项数据的类。数组就是用来管理多项数据的,但数组有很多限制,比如说,长度固定,插入、删除操作效率比较低。计算机技术有一门课程叫数据结构,专门讨论管理数据的各种方式。

这些数据结构在 Java 中的实现主要就是 Java 中的各种容器类,甚至,Java 泛型的引入主要也是为了更好的支持 Java 容器。后续章节我们会详细讨论主要的 Java 容器,本节我们先自己实现一个非常简单的 Java 容器,来解释泛型的一些概念。

我们来实现一个简单的动态数组容器,所谓动态数组,就是长度可变的数组,底层数组的长度当然是不可变的,但我们提供一个类,对这个类的使用者而言,好像就是一个长度可变的数组,Java 容器中有一个对应的类 ArrayList,本节我们来实现一个简化版。

来看代码:

public class DynamicArray<E> {
   
    private static final int DEFAULT_CAPACITY = 10;

    private int size;
    private Object[] elementData;

    public DynamicArray() {
   
        this.elementData = new Object[DEFAULT_CAPACITY];
    }

    private void ensureCapacity(int minCapacity) {
   
        int oldCapacity = elementData.length;
        if(oldCapacity>=minCapacity){
   
            return;
        }
        int newCapacity = oldCapacity * 2;
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    public void add(E e) {
   
        ensureCapacity(size + 1);
        elementData[size++] = e;
    }

    public E get(int index) {
   
        return (E)elementData[index];
    }
    
    public int size() {
   
        return size;
    }

    public E set(int index, E element) {
   
        E oldValue = get(index);
        elementData[index] = element;
        return oldValue;
    }

}    

DynamicArray 就是一个动态数组,内部代码与我们之前分析过的 StringBuilder 类似,通过 ensureCapacity 方法来根据需要扩展数组。作为一个容器类,它容纳的数据类型是作为参数传递过来的,比如说,存放 Double 类型:

DynamicArray<Double> arr = new DynamicArray<Double>();
Random rnd = new Random();
int size = 1+rnd.nextInt(100);
for(int i=0; i<size; i++){
   
    arr.add(Math.random()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值