java基础知识梳理&泛型初探

目录

概述

范型的使用

类型参数

类型通配符

泛型方法

泛型类

限定类型参数上限

上界通配符(Upper Bounds Wildcards),用来限定泛型的上界。

下界通配符(Lower Bounds Wildcards),用来限定泛型的下界。

范型的特点

范型是类型擦除的

不能创建一个范型类型实例

不能初始化范型数组

基本类型不能做类型参数

static 的语境不能引用类型变量


概述

所谓范型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。
范型可以减少强制类型的转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,正是因为有这些优点,自从 Java 引入范型后,项目的编码规则上便多了一条:优先使用范型。

范型的使用

类型参数

类型参数就是我们在定义泛型类或者方法是动态指定的参数。

类型参数的命名规则

类型参数名称命名为单个大写字母,比如 Collection<E>
但是这个命名规则我们一般会遵循一般的约定,以便可以在使用普通类或接口名称时能够容易地区分类型参数,增加代码的可读性。
以下是常用的类型参数名称列表:

  • E:元素 Element,主要由Java集合(Collections)框架使用。
  • K:键 Key,主要用于表示映射中的键的参数类型。
  • V:值 Value,主要用于表示映射中的值的参数类型。
  • N:数字 Number,主要用于表示数字。
  • T:类型,主要用于表示第一类通用型参数。
  • S:类型,主要用于表示第二类通用类型参数。
  • U:类型,主要用于表示第三类通用类型参数。
  • V:类型,主要用于表示第四个通用类型参数。

类型通配符

类型通配符一般是使用 ? 代替具体的类型参数,表示未知类型。例如 List<?> 在逻辑上是 List<String>List<Integer> 等所有 List<具体类型实参> 的父类。
类型通配符的形式有 <?><? extends Class><? super Class>

基本使用

为了表示各种范型 List 的父类,我们需要使用类型通配符,类型的通配符就是一个问号 ?,将它作为类型实参传给 List集合:List<?>,就标示未知类型元素的 List,它的元素类型可以匹配任何类型。

public void testGeneric() {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Shape> shape = new ArrayList<Shape>();
    name.add("icon");
    age.add(18);
    shape.add(new Shape());
    getData(name);
    getData(age);
    getData(shape);
}
public static void getData(List<?> data) {
    Log.e("Test","data :" + data.get(0));
}
public static class Shape {
    @Override
    public String toString() {
        return "Shape";
    }
}

编译没问题,运行结果:

E/Test: data :icon
E/Test: data :18
E/Test: data :Shape

因为 getData() 方法的参数是 List 类型的,所以 name,age,shape 都可以作为这个方法的实参,这就是通配符的作用。

上面程序中使用的 List<?>,其实这种写法可以适用于任何支持范型声明的接口和类,比如 Set<?>Map<?,?>等。

设定类型通配符的上限

假设有下面的使用场景,我们不想使 List<?> 使任何范型 List 的父类,只想表示它是某一类范型List的父类,这时候我们就要限定通配符的上限了。
<? extends Class> 表示该通配符所代表的类型是 Class 类型本身或者它的子类。或者 <? extends T>
把上面的 getData 方法修改一下:

public void testGeneric() {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Shape> shape = new ArrayList<Shape>();
    List<Circle> circle = new ArrayList<Circle>();
    name.add("icon");
    age.add(18);
    shape.add(new Shape());
    getData(name);  // 编译报错
    getData(age);   // 编译报错
    getData(shape); // 编译通过
    getData(circle);// 编译通过
}
public static void getData(List<? extends Shape> data) {
    Log.e("Test","data :" + data.get(0));
}
public static class Shape {
    @Override
    public String toString() {
        return "Shape";
    }
}
public static class Circle extends Shape {
    @Override
    public String toString() {
        return "Circle";
    }
}

前面两个用法就会报错:

Error:(74, 17) 错误: 不兼容的类型: List<String>无法转换为List<? extends Shape>
Error:(75, 17) 错误: 不兼容的类型: List<Integer>无法转换为List<? extends Shape>

设定类型通配符的下限

public static void getData(List<? super Shape> data) {
    Log.e("Test","data :" + data.get(0));
}

getData(name);  // 编译报错
getData(age);   // 编译报错
getData(shape); // 编译通过
getData(circle);// 编译报错

泛型方法

范型方法就是在声明方法时定义一个或多个类型形参,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
范型方法的类型作用域是整个方法。
范型方法的用法格式如下:

修饰符 <T, S> 返回值类型 方法名 (形参列表) {
}

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在上面例子中的<T, S>)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数可以用来声明方法参数。
  • 类型参数能被用来声明返回值类型。

先来看一个范型参数来声明方法参数的例子:

public <T> String getData(T t) {
    return String.valueOf(t);
}

再来看一个范型参数来声明返回值类型的例子:

private Map<String, Object> mDatas = new ArrayMap<>();
public <T> T getData(String name) {
    return (T) mDatas.get(name);
}

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。
一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

public class Paradigm<T> {
    private T t;
    public Paradigm() {
    }
    public Paradigm(T t) {
        this.t = t;
    }
    public T getT() {
        return t;
    }
    public Paradigm<T> setT(T t) {
        this.t = t;
        return this;
    }
}

限定类型参数上限

Java 范型不仅允许在使用通配符形参时设定上限,而且也可以在定义类型参数时设定上限,用于表示传给该类型形参的实际类型要么是该类型上限,要么使该类型的子类。
这种做法可以用在范型方法和范型类中。
用法:<U, T extends Class1>

public <T extends Shape & Serializable> String getData(T t) {
    return String.valueOf(t);
}
public static class Shape {
    @Override
    public String toString() {
        return "Shape";
    }
}

形如 <U, T extends Class1 & Interface1> 表示 T 是继承了 Class1 的类以及实现了 Interface1,后面的接口可以有多个,因为 Java 是单继承,因此父类只能有1个。类要写在接口的前面。

<? extends T> 是指上界通配符(Upper Bounds Wildcards),用来限定泛型的上界。

        Paradigm<? extends Number> numberParadigm = new Paradigm<Integer>(Integer.valueOf(123));

        Number t2 = numberParadigm.getT();
        //不能存入任何元素
        numberParadigm.setT(Integer.valueOf(123));    //Error  编译错误
        numberParadigm.setT(Float.valueOf(0.5F));    //Error  编译错误
        numberParadigm.setT(Double.valueOf(0.5F));    //Error  编译错误

<? extends Number> 这里的问号「?」即为泛型持有的类型,它的范围必须是Number类的子类型或者本身,但不可以是Number的超类或者其它不相关的类型。

<? extends Number>会使Paradigm的setT()方法失效。但getT()方法还有效。

原因是编译器只知道容器内是Nubber或者它的派生类,但具体是什么类型不知道。可能是Nubber?可能是Integer?也可能是Float,Double?编译器在看到后面用Paradigm<Integer>赋值以后,容器里没有被标上有“Integer”。而是标上一个占位符:CAP#1,来表示捕获一个Nubber或Nubber的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Integer或者Float或者Nubber编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。

<? super T> 是指下界通配符(Lower Bounds Wildcards),用来限定泛型的下界。

        Paradigm<? super Number> numberParadigm = new Paradigm<Number>(Integer.valueOf(123));

        //存入元素正常
        numberParadigm.setT(Integer.valueOf(123));    
        numberParadigm.setT(Float.valueOf(0.5F));    
        numberParadigm.setT(Double.valueOf(0.5F));    

        //读取出来的东西只能存放在Object类里
        Object o1 = numberParadigm.getT();
        Integer o2 = numberParadigm.getT();  //Error
        Float o3 = numberParadigm.getT();  //Error

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Nubber的基类,那往里存粒度比Nubber小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。

自定义泛型T和类型通配符?的区别

首先他们都表示不确定的类型。
自定义泛型 T 可以在方法体内进行各种操作,比如:

T t = it.next();
System.out.println(t);

也可以方法返回值:

public  static <T> T getData(List<T> data){
    Log.e("Test","data :" + data.get(0));
    return data.get(0);
}

也就是说,当你仅仅想表达一种不确定类型时可以用类型通配符?,但你如果相对类型参数进行操作或者是想表达两个类型参数之间或者参数与返回值之间关系时,这时就要用自定义泛型 T。

范型的特点

范型是类型擦除的

Java 的范型在编译器有效,在运行期被删除,也就是说所有的反省参数类型在编译期后都会被清除掉。
下面看一段代码:

public void listMethod(List<String> strings) {
}
public void listMethod(List<Integer> strings) {
}

这段代码是否能编译呢?
事实上,这段代码时无法编译的,编译时报错信息如下:

`listMethod(List<String>)` clashes with `listMethod(List<Integer>)`; both methods have same erasure

此错误信息是说 listMethod(List<String>) 方法在编译时擦除类型后的方法是 listMethod(List<E>),它与另一个方法相冲突。这就是 Java 范型擦除引起的问题:在编译后所有的范型类型都会做相应的转化:
转化规则如下:

  • List<String>List<Integer>List<T>擦除后的类型为List
  • List<String>[]擦除后的类型为List[]
  • List<? extends E>、List<? super E> 擦除后的类型为 List<E>
  • List<T extends Serializable & Cloneable> 擦除后为 List<Serializable>

范型的擦除还表现在当把一个具有范型信息的对象赋给另一个没有范型信息的变量时,所有再尖括号之间的类型信息都将被扔掉。
比如:一个 List<String> 类型被转换为 List,则该 List 对集合元素的类型检查变成了类型变量的上限(即 Object)。
下面用一个例子来示范这种擦除:

        public void testGenericErasure() {
        // 传入 Integer 作为类型形参的值
        Paradigm<Integer> paradigm = new Paradigm<>(8);
        // paradigm 的 get 方法返回 Integer 对象
        Integer size = paradigm.get();
        // 把 paradigm 对象赋值给b变量,此时会丢失<>里的类型信息
        Paradigm b = paradigm;
        //这个代码会引起编译错误
//        Integer size1 = b.get();
        // b只知道size的类型是Number,但具体是Number的哪个子类就不清楚了。
        //下面的用法是正确的
        Number size2 = b.get();
    }

    public class Paradigm<T extends Number> {
        private T size;
        public Paradigm(T size) {
            this.size = size;
        }
        public void add(T size) {
            this.size = size;
        }
        public T get() {
            return size;
        }
    }

类型转换:

public void testGenericConvert() {
    List<Integer> li = new ArrayList<>();
    li.add(8);
    // 类型擦除
    List list = li;
    // 类型转换
    List<String> ls = list;
    // 下面的代码会引起运行时异常
    // Caused by: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    Log.e("Test",ls.get(0));
}

明白了这些,对下面的代码就容易理解了:

List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
Log.e("Test",""+stringList.getClass().equals(integerList.getClass()));

返回结果为 true。 List<String>List<Integer> 擦除后的类型都是 List,没有任何区别。
之所以设计成可擦除的,有下面两个原因:

  • 避免JVM大换血。由于范型是Java5以后才支持的,如果JVM也把范型类型延续到运行期,那么JVM就需要进行大量的重构工作了。也就是说,Java 中的泛型机制其实就是一颗语法糖,并不涉及JVM的改动。
  • 版本兼容问题。在编译器擦除可以更好地支持原生类型,在Java5或者Java6平台上,即使声明一个List这样的原生类型也是支

不能创建一个范型类型实例

如果 T 是一个类型变量,那么下面的语句是非法的:

T obj = new T();

T 由它的限界代替,这可能是 Object,或者是抽象类,因此对 new T() 的调用没有意义。

不能初始化范型数组

数组元素的类型不能包含类型变量或者类型形参,除非是无上限的类型通配符。但是可以声明元素类型包含类型变量或类型形参的数据。
也就是说,下面的代码是OK的:

List<String>[] stringList ;

public class Paradigm<T extends Number> {
    private List<T> list = new ArrayList<T>();
}

下面的代码,等号后面的代码是非法的:

List<String>[] stringList = new ArrayList<String>[10];

基本类型不能做类型参数

因此,List<int> 是非法的,我们必须使用包装类。

static 的语境不能引用类型变量

在一个范型类中,static 方法和 static 域均不可以引用类的类型变量,因为类型擦除后类型变量就不存在了。而且,static 域在该类的诸范型实例之间是共享的。因此,static 的语境不能引用类型变量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355