泛型

前言

泛型也属于JDK1.5之后的新特性,在这篇文章中将介绍一下可变参数列表和foreach循环以及泛型的使用.泛型是重点

可变参数列表

现在有一个需求:
设计一个方法用来计算若干个整数之和,整数的个数由使用者决定
在早期我们只能通过数组的方式去实现他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AddMoreNum {

public static int add(int[] arr){
int result = 0;
for(int i=0;i<arr.length;i++){
result = result+arr[i];
}
return result;
}

public static void main(String[] args) {
System.out.println(add(new int[]{1,2,3,4,5,6}));
System.out.println(add(new int[]{1,2,3}));
}
}

从JDK.5之后我们再也不需要这样做了,JDK1.5之后提出了可变参数列表,我们可以一次传入不定数量的参数
还是上面那个例子,可变参数的写法如下:

1
2
3
4
5
6
7
8
//可变参数的写法
public static int add2(int... num){
int result =0;
for(int i=0;i<num.length;i++){
result = result+num[i];
}
return result;
}

和数组的方法一样能够得到我们想要的结果
这个参数上使用的”…”实际上表示的就是一个数组的结构,所以在使用参数的时候还是要和使用数组的方式相同,通过下标去取出数据
上面演示的是传递一种类型的数据,还可以传递多种不同类型的数据,但是可变参数部分的数据必须是一个类型的,并且可变参数要放在最后面

1
2
3
4
5
6
7
8
//传递不同类型的数据
public static void print(String str, int... num) {

for (int i = 0; i < num.length; i++) {
System.out.print(num[i]+" ");
}
System.out.println(str);
}

foreach循环

特点: 无需在通过索引来取得数据
foreach循环的基本格式

1
2
3
for(数据类型 临时变量 : 数组(集合)) {
// 循环次数为数组长度,而每一次循环都会顺序取出数组中的一个元素赋值给临时变量
}

这种增强的for循环可以很好的避免数组越界的发生,但是这种数组的操作只适合简单输出模式
foreach遍历数组

1
2
3
4
5
6
7
8
//foreach实现数组内元素相加
public static int add22(int[] arr) {
int result = 0;
for(int i:arr){
result = result+i;
}
return result;
}

泛型

泛型的作用: 解决了程序的参数转换问题

问题引出

需求:
假设需要你定义一个描述坐标的程序类Point,需要提供两个属性x、y。对于这两个属性的内容可能有如下选择:

  1. x = 10、y = 20 ;
  2. x = 10.1、y = 20.1 ;
  3. x = 东经80度、y = 北纬20度

分析:Point类中的属性有可能是int,有可能是double,还有可能是String,所以我们只能使用Object去保存这些类型,让他们在传入数据的时候向上转型,拿出数据的时候向下转型
定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Point{
private Object x;
private Object y;

public Object getX() {
return x;
}

public void setX(Object x) {
this.x = x;
}

public Object getY() {
return y;
}

public void setY(Object y) {
this.y = y;
}
}

设置整型坐标:

1
2
3
4
5
6
Point point = new Point();
//设置为整形
point.setX(10); //自动装箱向上转型为Object
point.setY(20);
int x = (Integer) point.getX(); //强制向下转型为Integer并自动拆箱
int y = (Integer) point.getY();

设置字符串

1
2
3
4
5
6
//设置为字符串
Point point = new Point();
point.setX("北纬100");
point.setY("东京120");
String x1 = (String)point.getX();
String y1 = (String)point.getY();

看似问题已经解决,既可以设置为int型又可以设置为String类型.但其实在这里存在着很大的隐患,向下转型的过程中总是存在着风险的
当我们传入的两个坐标一个为整型,一个为String类型时,风险便会显现
—>

1
2
3
4
5
6
Point point = new Point();
point.setX(10);
point.setY("北纬20度");
String x = (String) point.getX();
String y = (String) point.getY();
System.out.println(x+" "+y);

程序运行出现异常: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
在设置参数的时候x为整型,y为String类型;在取出参数的时候将两个值都强转为String类型;y的强转没有问题,但是x的强转明显是有问题的,因为一个int类型和String类型之间是没有关系的,强转必定会出现错误,并且这个异常在编译阶段是检测不出来了,这就是使用Object带来的隐患
这时候就用到了泛型

泛型类的基本使用

泛型: 泛型指的就是在类的定义中,类属性的类型,方法的参数的具体类型都不明确指定,而是在类使用的时候才进行定义
泛型类的基本语法;

1
2
3
class Type<T>{
private T p1;
}

尖括号<>中的T被称作参数类型,用于指代任何类型,他可以看作是一个占位标记符,当我们实际给这个类的属性传入参数类型时他就会被代替。
实际上T可以使用任何字母代替,但是为了编程规范,一般使用如下字母分别代替不同的含义:

  • T 代表一般的任何类。
  • E 代表 Element 的意思,或者 Exception 异常的意思。
  • K 代表 Key 的意思
  • V 代表 Value 的意思,通常与 K 一起配合使用。
  • S 代表 Subtype 的意思,文章后面部分会讲解示意。
    如果一个类被的形式定义,那么他就是一个泛型类.

泛型类的使用

1.定义泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person<T>{
private T name;
private T address;

public T getName() {
return name;
}

public void setName(T name) {
this.name = name;
}

public T getAddress() {
return address;
}

public void setAddress(T address) {
this.address = address;
}
}

可以看到在定义阶段我们并没有具体指定name,address的类型,而是使用了泛型的标记符T代表参数类型
2.实例化泛型类对象,传入参数类型

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
//在实际使用Person类时传入属性的类型
Person<String> person = new Person<String>();
person.setAddress("西安");
person.setName("小明");
System.out.println(person.getName()+" "+person.getAddress());
}
}

这就是泛型的基本使用方法,在这里需要注意一个问题:

  • 使用泛型传递数据类型的时候,传递的这个数据类型必须是一个类,也就是说不能传递简单数据类型,需要传递他们的包装类型

泛型类可以接收多个类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
Person<String,Integer> person = new Person<String,Integer>();
......
}
}

class Person<T,E>{
private T name;
private E id;
......
}

坐标问题使用泛型解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
public static void main(String[] args) {
Position<String> position = new Position<String>();
position.setX("东京20度");
position.setY("北纬30");
String x = position.getX(); //再也不需要向下转型了
String y = position.getY();
}
}

class Position<T>{
private T x;
private T y;

public T getX() {
return x;
}

public void setX(T x) {
this.x = x;
}

public T getY() {
return y;
}

public void setY(T y) {
this.y = y;
}
}

泛型的出现彻底改变了向下转型的需求.引入泛型后,如果在类实现时设置了泛型类型, 那么就是设置的类型,否则默认为Object类型

泛型方法

泛型不仅可以用来定义类,还可以单独用来定义方法
泛型方法的定义:

1
2
3
4
5
public class{
public <T>void print(T t){
system.out.println(t);
}
}

泛型方法中需要注意的是需要写到返回值前面.中的T称为类型参数,而方法中的T,被称为参数化类型,他不是运行时的真正参数 。
当然,声明的参数类型也可以作为返回值

1
2
3
4
5
public class{
public <T>T print(T t){
system.out.println(t);
}
}

泛型方法可以和泛型类共存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test2 {
public static void main(String[] args) {
MyClass<String> myClass = new MyClass<String>();
myClass.setId("adad15");

myClass.print(1);
System.out.println(myClass.getId());
}
}

class MyClass<T>{

private T id;

public <T>void print(T t){
System.out.println(t);
}

public T getId() {
return id;
}

public void setId(T id) {
this.id = id;
}
}

注意:泛型类的参数类型T和类中泛型方法中的T不是一个T!!,泛型类的参数类型T他只会作用于类中的属性的类型,他和方法中的参数类型是两个互相独立,互不干扰的泛型类型,泛型方法始终以自己定义的类型参数为准
例如上例中泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不相干。
为了更好的区分可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass<T>{

private T id;

public <E>void print(E t){
System.out.println(t);
}

public T getId() {
return id;
}

public void setId(T id) {
this.id = id;
}
}

通配符

使用泛型解决了ClassCastException的问题又会出现参数统一的问题. 看下面一串代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test3 {
public static void print(Message<String> msg){
System.out.println(msg.getMsg());
}

public static void main(String[] args) {
Message<String> message = new Message<String>();
message.setMsg("hello");
print(message);
}
}

class Message<T>{
private T msg;

public T getMsg() {
return msg;
}

public void setMsg(T msg) {
this.msg = msg;
}
}

如果此时泛型类的参数类型改为Integer,那么显然print方法就会出错,因为他只能接受String类型的Message对象
那么如何做到此处接收的对象类型能够随着泛型类所具体使用的参数类型的变化而变化呢?这时就需要使用到通配符 ? 来处理此问题 .
通配符的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test3 {
public static void print(Message<?> msg){
//由于类型的不确定,值不能修改
//msg.setMsg("aaa");
System.out.println(msg.getMsg());
}

public static void main(String[] args) {
Message<String> message = new Message<String>();
message.setMsg("hello");
print(message);
}
}

此时使用通配符”?”描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改

?的两个子通配符

  • ? extends 类 : 设置通配符上限
    例如 ? extends Number ,表示只能够设置Number的子类,例如:Integer,Double等,最高到Number
  • ? super 类: 设置通配符下限
    例如: ? super String,表示只能够设置String类或其父类Object,最低到String

具体用法:
泛型上限的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test4 {

public static void print(Messagea<? extends Number> msg){
//值不能够改变
// msg.setMsg(50);
System.out.println(msg.getMsg());
}

public static void main(String[] args) {
Messagea<Integer> messagea = new Messagea<Integer>();
messagea.setMsg(100);
print(messagea);
}
}

class Messagea<T extends Number>{ //注意在此处设置泛型的上限
private T msg;

public T getMsg() {
return msg;
}

public void setMsg(T msg) {
this.msg = msg;
}
}

设置了上线,规定了传入的参数类型的范围,所以在print方法中就只能传入Number的子类,如果相传入一个String,就会出错,以此也规范了方法的使用
泛型下限的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test5 {

public static void print(Mess<? super String> mess){
//此时可以修改原内容
mess.setMsg("ad");
System.out.println(mess.getMsg());
}

public static void main(String[] args) {
Mess<String> mess = new Mess<String>();
mess.setMsg("hello");
print(mess);
}
}

class Mess<T>{
private T msg;

public T getMsg() {
return msg;
}

public void setMsg(T msg) {
this.msg = msg;
}
}

总结:上限可以用在声明,不能修改;下限只能用在方法参数,可以修改内容

泛型接口

泛型也可以定义在接口里面

1
2
3
interface Imessage<T>{
void print(T t);
}

泛型用在接口上意味着接口中的方法的参数都是该泛型类型
对于一个泛型接口,有两种实现方式
第一种:在子类实现接口时依然使用泛型

1
2
3
4
5
6
7
8
9
10
11
public class MesssImp{
public static void main(String[] args) {
MesssImpp<String> messsImpp = new MesssImpp();
messsImpp.print("hello");
}
}
class MesssImpp<T> implements Messs<T>{
public void print(T t) {
System.out.println(t);
}
}

说明: 此种情况下如果实现类在继承接口的时候没有继承泛型接口,而是直接”实现类 implements Messs”,则继承后的方法中的参数值为”public void print(Object o)”
第二种:在子类实现接口时不使用泛型而是直接给定类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test6 {

public static void main(String[] args) {
Messimp messimp = new Messimp();
messimp.print("hello");
}
}

class Messimp implements Messs<String>{

public void print(String s) {
System.out.println(s);
}
}

类型擦除

泛型信息只存在于代码的编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,这个过程就叫做类型擦除
通俗的来说,泛型类在JVM中就是一个普通类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义泛型接口
interface Message<T>{
void msg(T t);
}

public class MesssTest {
public static void main(String[] args) {
MesssTe<String> messsTe = new MesssTe();
MesssTe<Integer> messsTe1 = new MesssTe<Integer>();
System.out.println(messsTe.getClass() == messsTe1.getClass());
}
}

class MesssTe<T> implements Messs<T>{
public void print(T t) {
System.out.println(t);
}
}

运行结果: true
说明为 MyClass 和 MyClass 在 JVM 中的 Class 都是MyClass.class 。