Java泛型

简介: Java泛型

Java泛型


🧳1. 泛型类的定义🧳

一般的类和方法,只能使用具体的类型:要么是基本的类型,要么是自定义类型。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的舒服就会很大。----来源《Java编程思想》对泛型的介绍。

泛型实在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。

1.1 语法

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
        }
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
        }

了解: 【规范】类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

不能实例化泛型类型的数组

    public T[] objects = new T[10];//ERROR 1、不能实例化泛型类型的数组

1.2 简单示例

定义一个泛型类顺序表

// 演示泛型目的,没有做错误处理和扩容
public class MyArrayList<E> {
    private E[] array;
    private int size;
    public MyArrayList() {
// 泛型类型无法直接创建数组,具体的见下面的注意事项
        array = (E[])new Object[16];
        size = 0;
    }
    // 尾插
    public void add(E e) {
        array[size++] = e;
    }
    // 尾删
    public E remove() {
        E element = array[size - 1];
        array[size - 1] = null; // 将容器置空,保证对象被正确释放
        size--;
        return element;
    }
}

1.3 加入静态内部类的示例

定义一个泛型类链表

public class MyLinkedList<E> {
    public static class Node<E> {
        private E value;
        private Node<E> next;
        private Node(E e) {
            value = e;
            next = null;
        }
    }
    private Node<E> head;
    private int size;
    public MyLinkedList() {
        head = null;
        size = 0;
    }
    // 头插
    public void pushFront(E e) {
        Node<E> node = new Node<>(e);
        node.next = head;
        head = node;
        size++;
    }
    // 尾插
    public void pushBack(E e) {
        if (size == 0) {
            pushFront(e);
            return;
        }
        Node<E> cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = new Node<E>(e);
        size++;
    }
}

1.4 加入继承或实现的示例

定义一个泛型类顺序表

public interface MyList<E> {
    // 尾插
    void add(E e);
    // 尾删
    E remove();
}
public class MyArrayList<E> implements MyList<E> {
// TODO: 未完成
}

1.5泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

语法

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

上述的类型边界就是我们泛型的上界

示例

public class MyArray<E extends Number> {
...
}

只接受 Number 的子类型作为 E 的类型实参

MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型
error: type argument String is not within bounds of type-variable E
    MyArrayList<String> l2;
        ^
where E is a type-variable:
    E extends Number declared in class MyArrayList

了解: 没有指定类型边界 E,可以视为 E extends Object

不指定边界就默认为Object

1.5.1复杂举例

public class MyArray<E extends Comparable<E>> {
...
}

E必须是实现了Comparable接口的

1.6泛型的下界

泛型没有下界!!!

🌂2. 泛型类的使用🌂

2.1 语法

泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

2.2 示例

MyArrayList<String> list = new MyArrayList<String>();

2.3 类型推导(Type Inference)

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

MyArrayList<String> list = new MyArrayList<>(); // 可以推导出实例化需要的类型实参为 String

☂️3. 裸类型(Raw Type) (了解)☂️

3.1 说明

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型

MyArrayList list = new MyArrayList();

**注意:**我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

下面的类型擦除部分,我们也会讲到编译器是如何使用裸类型的。

3.2 未检查错误

MyArrayList<String> list = new MyArrayList(); // 自己永远不要这么用

上述代码,会产生编译警告

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

可以使用 @SuppressWarnings 注解进行警告压制

@SuppressWarnings("unchecked")

🧵4. 泛型类的定义-类型边界🧵

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

4.1 语法

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

4.2 示例

public class MyArrayList<E extends Number> {
...
}

只接受 Number 的子类型作为 E 的类型实参

MyArrayList<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArrayList<String> l2; // 编译错误,因为 String 不是 Number 的子类型
error: type argument String is not within bounds of type-variable E
        MyArrayList<String> l2;
        ^
where E is a type-variable:
        E extends Number declared in class MyArrayList

了解: 没有指定类型边界 E,可以视为 E extends Object

4.3 复杂一点的示例

定义一个泛型类搜索树

public class BSTree<K extends Comparable<K>, V> {
...
}

传入的 K 必须是 Comparable 的,并且是可以和另一个 K 类型做比较的,后边的 K 是对类型参数的使用了

🧶5. 类型擦除🧶

我们之前已经讲过,泛型是作用在编译期间的一种机制,实际上运行期间是没有这么多类的,那运行期间是什么类型呢?这里就是类型擦除在做的事情。

class MyArrayList<E> {
// E 会被擦除为 Object
}
class MyArrayList<E extends Comparable<E>> {
// E 被擦除为 Comprable
}

总结: 即类型擦除主要看其类型边界而定

了解: 编译器在类型擦除阶段在做什么?

  1. 将类型变量用擦除后的类型替换,即 Object 或者 Comparable
  2. 加入必要的类型转换语句
  3. 加入必要的 bridge method 保证多态的正确性

👓6. 泛型类的使用-通配符(Wildcards)👓

? 用于在泛型的使用,即为通配符

6.1通配符解决什么问题

**通配符是用来解决泛型无法协变的问题的,**协变指的就是如果 Student 是 Person 的子类,那么 List 也应该是 List 的子类。但是泛型是不支持这样的父子类关系的。

1、泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。

2、或者我们可以这么理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定,规定你能传哪些参数。

class Alg3 {
    public static <T> void print1(ArrayList<T> list) {
        for (T x: list) {
            System.out.println(x);
        }
    }
    public static void print2(ArrayList<?> list) {
        for (Object x: list) {
            System.out.println(x);
        }
    }
}

print1是泛型T来接受,他可以确定你传过来的是泛型T类型。

但是print2使用的是Object来接受,他不能确定你传过来的?是什么类型,所以他选择了使用万能类型Object来接受。

示例

package www.bit.java.test;
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 main(String[] args) {
        Message<String> message = new Message() ;
        message.setMessage("庐山风景真美");
        fun(message);
    }
    public static void fun(Message<String> temp){
        System.out.println(temp.getMessage());
    }
}

以上程序会带来新的问题,如果现在泛型的类型设置的不是String,而是Integer.

public class TestDemo {
    public static void main(String[] args) {
        Message<Integer> message = new Message() ;
        message.setMessage(99);
        fun(message); // 出现错误,只能接收String
    }
    public static void fun(Message<String> temp){
        System.out.println(temp.getMessage());
    }
}

我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符"?"来处理

范例:使用通配符

public class TestDemo {
    public static void main(String[] args) {
        Message<Integer> message = new Message() ;
        message.setMessage(55);
        fun(message);
    }
    // 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
    public static void fun(Message<?> temp){
//temp.setMessage(100); 无法修改!
        System.out.println(temp.getMessage());
    }
}

在"?"的基础上又产生了两个子通配符:

? extends 类:设置泛型上限

? super 类:设置泛型下限

6.2 通配符-上界

语法

只能调用传入参数的子类,不允许调用他们的父类

<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类

示例

// 可以传入类型实参是 Number 子类的任意类型的 MyArrayList 因为这里的? 继承了Number。
public static void printAll(MyArrayList<? extends Number> list) {
...
}
// 以下调用都是正确的
printAll(new MyArrayList<Integer>());
printAll(new MyArrayList<Double>());
printAll(new MyArrayList<Number>());
// 以下调用是编译错误的
printAll(new MyArrayList<String>());
printAll(new MyArrayList<Object>());

此时的list可以引用的子类对象很多,编译器无法确定你具体的类型的。 编译器为了安全起见,此时只允许你进行读取。

上述传入的是Number,调用Integer、Double和Number是可以的,但是调用String和Object是不可以的。

注意: 需要区分 泛型使用中的通配符上界 和 泛型定义中的类型上界

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

6.3 通配符-下界

语法

<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

示例

// 可以传入类型实参是 Integer 父类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? super Integer> list) {
        ...
        }
// 以下调用都是正确的
        printAll(new MyArrayList<Integer>());
        printAll(new MyArrayList<Number>());
        printAll(new MyArrayList<Object>());
// 以下调用是编译错误的
        printAll(new MyArrayList<String>());
        printAll(new MyArrayList<Double>());

假如我们引用Person,我们引用的对象肯定是Person或者Person的父类的集合,我们能够确定此时存储的元素的最小粒度比Person小的都可以。但是,你读取的时候,你知道是读取到的哪个子类吗?

添加元素时,只要添加的时Person或者Person的子类就可以。

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

为什么

Person person = arrayList1.get(0);

//Student student = arrayList1.get(0);

注:Student是Person的子类

不可以取出arrayList的元素?

答:因为存放的时候可以通过多态来存放Person的子类,但是取出的时候,只能取出Person或者Person的父类,不能向下引用子类

🥼7. 泛型中的父子类型(重要)🥼

public class MyArrayList<E> { ... }
// MyArrayList<Object> 不是 MyArrayList<Number> 的父类型
// MyArrayList<Number> 也不是 MyArrayList<Integer> 的父类型
// 需要使用通配符来确定父子类型
// MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型
// MyArrayList<? extends Number> 是 MyArrayList<Integer> 的父类型

理论上来说:Object是所有类的父类

为什么上述代码的Object不是Number的父类呢,因为编译完成后我们的类型都被擦除了

🦺8. 泛型方法🦺

8.1 定义语法

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

8.2 示例

一般的泛型方法需要new对象,new完对象之后才可以使用我们的Alg方法

class Alg<T extends Comparable<T>> {
    public T findMax(T[] array) {
        T max = array[0];
        for (int i = 0; i < array.length; i++) {
            if (max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Alg<Integer> alg = new Alg<>();
        Integer[] array = {1,2,3,4,7,5,6,9,100,97,123};
        System.out.println(alg.findMax(array));
        Alg<String> alg1 = new Alg<>();
        String[] array1 = {"abc","xxhljq","hello"};
        System.out.println(alg1.findMax(array1));
    }

加入了static之后的泛型方法变为了静态的泛型方法,不需要new对象,直接就可以调用Alg2的方法。

class Alg2 {
    public static <T extends Comparable<T>>T findMax(T[] array) {
        T max = array[0];
        for (int i = 0; i < array.length; i++) {
            if (max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Integer[] array = {1,2,3,4,7,5,6,9,100,97,123};
        //System.out.println(Alg2.findMax(array));  不写<Integer>我们的Alg2方法也会自己推导出array的类型是Integer
        System.out.println(Alg2.<Integer>findMax(array));
        String[] array1 = {"abc","xxhljq","hello"};
        //System.out.println(Alg2.findMax(array1));  不写<String>我们的Alg2方法也会自己推导出array的类型是String
        System.out.println(Alg2.<String>findMax(array1));
    }
}

8.3 使用示例-可以类型推导

Integer[] a = { ... };
        swap(a, 0, 9);
        String[] b = { ... };
        swap(b, 0, 9);

8.4 使用示例-不使用类型推导

Integer[] a = { ... };
        Util.<Integer>swap(a, 0, 9);
        String[] b = { ... };
        Util.<String>swap(b, 0, 9);

👔9. 泛型的限制👔

泛型类型参数不支持基本数据类型

无法实例化泛型类型的对象

无法使用泛型类型声明静态的属性

无法使用 instanceof 判断带类型参数的泛型类型

无法创建泛型类数组

无法 create、catch、throw 一个泛型类异常(异常不支持泛型)

泛型类型不是形参一部分,无法重载

👕10. 完整定义一份泛型类支持的搜索树(不使用 Comparator)👕

import java.util.*;
public class BSTree<K extends Comparable<K>, V> {
    private static class Entry<K, V> {
        private K key;
        private V value;
        private Entry<K, V> left = null;
        private Entry<K, V> right = null;
        private Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
        @Override
        public String toString() {
            return String.format("{%s=%s}", key, value);
        }
    }
    private Entry<K, V> root = null;
    public V get(K key) {
        Entry<K, V> cur = root;
        while (cur != null) {
            int r = key.compareTo(cur.key);
            if (r == 0) {
                return cur.value;
            } else if (r < 0) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }
        return null;
    }
    public V put(K key, V value) {
        if (root == null) {
            root = new Entry<>(key, value);
            return null;
        }
        Entry<K, V> parent = null;
        Entry<K, V> cur = root;
        while (cur != null) {
            int r = key.compareTo(cur.key);
            if (r == 0) {
                V oldValue = cur.value;
                cur.value = value;
                return oldValue;
            } else if (r < 0) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }
        Entry<K, V> entry = new Entry<>(key, value);
        if (key.compareTo(parent.key) < 0) {
            parent.left = entry;
        } else {
            parent.right = entry;
        }
        return null;
    }
    public V remove(K key) {
        Entry<K, V> parent = null;
        Entry<K, V> cur = root;
        while (cur != null) {
            int r = key.compareTo(cur.key);
            if (r == 0) {
                V oldValue = cur.value;
                removeEntry(parent, cur);
                return oldValue;
            } else if (r < 0) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }
        return null;
    }
    private void removeEntry(Entry<K, V> parent, Entry<K, V> cur) {
        if (cur.left == null) {
            if (cur == root) {
                root = cur.right;
            } else if (cur == parent.left) {
                parent.left = cur.right;
            } else {
                parent.right = cur.right;
            }
        } else if (cur.right == null) {
            if (cur == root) {
                root = cur.left;
            } else if (cur == parent.left) {
                parent.left = cur.left;
            } else {
                parent.right = cur.left;
            }
        } else {
            Entry<K, V> gParent = cur;
            Entry<K, V> goat = cur.left;
            while (goat.right != null) {
                gParent = goat;
                goat = goat.right;
            }
            cur.key = goat.key;
            cur.value = goat.value;
            if (goat == gParent.left) {
                gParent.left = goat.left;
            } else {
                gParent.right = goat.left;
            }
        }
    }
    public static interface Function<T> {
        void apply(T entry);
    }
    public static <K, V> void preOrderTraversal(Entry<K, V> node, Function<Entry<K, V>>
            func) {
        if (node != null) {
            func.apply(node);
            preOrderTraversal(node.left, func);
            preOrderTraversal(node.right, func);
        }
    }
    public static <K, V> void inOrderTraversal(Entry<K, V> node, Function<Entry<K, V>>
            func) {
        if (node != null) {
            inOrderTraversal(node.left, func);
            func.apply(node);
            inOrderTraversal(node.right, func);
        }
    }
    public void print() {
        System.out.println("前序遍历: ");
        preOrderTraversal(root, (n) -> {
            System.out.print(n.key + " ");
        });
        System.out.println();
        System.out.println("中序遍历: ");
        inOrderTraversal(root, (n) -> {
            System.out.print(n.key + " ");
        });
        System.out.println();
    }
    public static void main(String[] args) {
        BSTree<Integer, String> tree = new BSTree<>();
        int count = 0;
        Random random = new Random(20190915);
        for (int i = 0; i < 20; i++) {
            int key = random.nextInt(200);
            String value = String.format("Value of %d", key);
            if (tree.put(key, value) == null) {
                count++;
            }
        }
        System.out.println("一共插入 " + count + " 个结点");
        tree.print();
    }
}

感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘

目录
相关文章
|
3月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
75 2
|
13天前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
12 0
[Java]泛型
|
22天前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
10 1
|
29天前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
26 5
|
27天前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
17 1
|
2月前
|
Java 编译器 容器
Java——包装类和泛型
包装类是Java中一种特殊类,用于将基本数据类型(如 `int`、`double`、`char` 等)封装成对象。这样做可以利用对象的特性和方法。Java 提供了八种基本数据类型的包装类:`Integer` (`int`)、`Double` (`double`)、`Byte` (`byte`)、`Short` (`short`)、`Long` (`long`)、`Float` (`float`)、`Character` (`char`) 和 `Boolean` (`boolean`)。包装类可以通过 `valueOf()` 方法或自动装箱/拆箱机制创建。
34 9
Java——包装类和泛型
|
29天前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
18 2
|
2月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
26天前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
13 0
|
2月前
|
存储 安全 搜索推荐
Java中的泛型
【9月更文挑战第15天】在 Java 中,泛型是一种编译时类型检查机制,通过使用类型参数提升代码的安全性和重用性。其主要作用包括类型安全,避免运行时类型转换错误,以及代码重用,允许编写通用逻辑。泛型通过尖括号 `&lt;&gt;` 定义类型参数,并支持上界和下界限定,以及无界和有界通配符。使用泛型需注意类型擦除、无法创建泛型数组及基本数据类型的限制。泛型显著提高了代码的安全性和灵活性。