包装类and泛型

一.包装类

        包装类: Java是一个面向对象的编程语言,但是在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。

1.1 基本数据类型和对应的包装类

除了 Integer Character, 其余基本类型的包装类都是首字母大写。 

1.2 装箱和拆箱 

        什么是装箱和拆箱呢?所谓装箱就是将基本数据类型转化为包装类型,那么拆箱就是将包装类型转化为基本数据类型。 

        以基本数据类型int为例:

int i = 10;
// 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);
// 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();

1.Integer当中的的valueOf(int i)方法其实是将一个基本数据类型转化为对应的包装类型,即装箱方法

2.Integer的intValue()方法则是将一个包装类型转化为对应的基本数据类型,即拆箱方法

1.3 自动装箱和自动拆箱

        可以看到在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制 

int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱

1.4 装箱与自动装箱的区别 

        我们讲的装箱实际上是利用了Integer的构造方法Integer(int value),即Integer c=new Integer(1)。而我们所说的自动装箱(或者叫隐式装箱),是直接给Integer赋值,即Integer d=1,在编译的时候,会调用Integer.valueOf()方法完成装箱。相较而言,自动装箱可能比装箱具有更高的效率,这体现在了自动装箱的缓存上。

面试题

        下列代码输出什么,为什么? 

public static void main(String[] args) {
    Integer a = 127;
    Integer b = 127;
    Integer c = 128;
    Integer d = 128;
    System.out.println(a == b);
    System.out.println(c == d);
}

        看到上面这四个值,有些同学就直接发言:a和b,c和d的值两两相等,应该都返回true才对。

        其实不然,仔细一点我们可以发现:对于第一个比较 a == b,a 和 b 都是值为 127Integer 对象。在 Java 中,-128 127 之间的 Integer 对象会被缓存并重复使用。因此 a 和 b 实际上引用的是缓存中的同一个对象,因此结果为 true;对于第二个比较 c == d,尽管 c 和 d 的值相同,为 128,但它们在内存中并不是同一个对象,因为 Java 仅缓存从 -128127 Integer 对象。因此 c 和 d 引用的是不同的对象,结果为 false。 


二.泛型 

2.1 认识泛型 

        那么什么是泛型呢?

        在《java编程思想》这本书中有提到对泛型的介绍:一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

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

2.2 语法

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

2.3 引出泛型

        思考一下,我们能不能实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?

        我的思路如下:在数组中,只能存放指定类型的元素,比如

int[ ] array = new int[10]

String[ ] strs = new String[10]

        所有类的父类,都默认为Object类,那么数组是否可以创建为Object呢?我们来做个测试:

class MyArray {
    public Object[] array = new Object[10];
    public Object getPos(int pos) {
     return this.array[pos];
       }
    public void setVal(int pos,Object val) {
     this.array[pos] = val;
       }
    }
    public class TestDemo {
     public static void main(String[] args) {
      MyArray myArray = new MyArray();
       myArray.setVal(0,10);
       myArray.setVal(1,"hello");//字符串也可以存放
       String ret = myArray.getPos(1);//编译报错
       System.out.println(ret);
      }
    }

        上述代码中,在TestDemo 类中,我们尝试将 myArray.getPos(1) 的结果分配给 String 变量 ret,但是 MyArray 类中的 getPos() 方法返回了一个 Object。当您尝试将 Object 分配给 String 时,我们就需要明确转换它。所以我们需要将代码进行更正: 

class MyArray<T> {
    public T[] array = (T[])new Object[10];
                       //这里不能new泛型类型的数组
     public T getPos(int pos) {
      return this.array[pos];
     }
    public void setVal(int pos,T val) {
     this.array[pos] = val;
    }
 }
    public class TestDemo {
     public static void main(String[] args) {
      MyArray<Integer> myArray = new MyArray<>();
                                     //类型后加入 <Integer> 指定当前类型
       myArray.setVal(0,10);
       myArray.setVal(1,12);
       int ret = myArray.getPos(1);
       //不需要进行强制类型转换
       System.out.println(ret);
       myArray.setVal(2,"bit");//代码编译报错,此时因为在new MyArray<>()处指定类当前的类型,此时编译器会在存放元素的时候帮助我们进行类型检查。
      }
 }

        在通过修改上述代码中我们可以发现:虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

2.4 泛型类的使用

2.4.1 语法 

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

2.4.2 示例

MyArray<Integer> list = new MyArray<Integer>();

 泛型只能接受类,所有的基本数据类型必须使用包装类。

2.4.3 类型推导(Type Inference) 

        当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写:
MyArray<Integer> list = new MyArray<>(); //可以推导出实例化需要的类型实参为 Integer

小结:

1. 泛型是将数据类型 参数化 ,进行传递。
2. 使用 <T> 表示当前类是一个泛型类
3. 泛型目前为止的优点:数据类型参数化,编译时 自动进行类型检查和转换。

三.泛型的编译 

3.1 擦除机制

        那么,泛型到底是怎么编译的?这个问题,也是曾经的一个面试问题。泛型本质是一个非常难的语法,要理解好他还是需要一定的时间打磨。

        通过上面的学习,我们已经了解到泛型的本质是将数据类型参数化,它是通过擦除的方式来实现的,编译器会在编译的时候擦除代码中所有泛型语法并且做出相应的一些类型的转换动作。也就是说,泛型信息它只会存在于代码编译阶段,编译阶段一结束,泛型有关的信息就会被擦除(专业术语:类型擦除)。 

        接下来我们通过调用命令:javap -c 来查看字节码文件,所有的T都是Object

在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制 

 3.2 答疑

        问题1:为什么 T[ ] ts = new T[5] 是不对的,编译的时候,替换为Object,不是相当于:Object[ ] ts = new Object[5]吗?

       解答:在 Java 中,泛型是在编译时进行类型擦除的。这意味着在编译时,所有的泛型类型都会被替换为它们的上限(对于没有明确指定上限的情况,默认上限是Object 所以,当你尝试声明 T[] ts = new T[5] 这样的语句时,编译器无法确定 T 到底是什么类型,因为在编译时泛型信息已经被擦除了。因此,编译器无法将 T 替换为具体的类型。这就是为什么这样的语句在 Java 中是不允许的。相反,你可以使用 Object[] ts = new Object[5],这是因为在数组声明中,数组的类型是已知的,因此可以创建一个 Object 类型的数组,这样的语句是合法的。

        问题2:类型擦除,一定是把T变成Object吗?

        解答:在 Java 中,类型擦除将泛型信息擦除为它们的上限类型。对于没有明确指定上限类型的泛型,其默认上限是Object 。但是,对于明确指定了上限类型的泛型,类型擦除会将泛型信息替换为其上限类型。例如,如果你声明一个泛型类 MyClass<T extends Number> {...},那么在编译时,所有的 T 将被替换为 Number。这意味着任何在 MyClass 中使用的 T 都将被视为 Number 类型。所以,类型擦除不一定会将泛型信息替换为Object ,而是替换为泛型类型的上限类型。

        问题3:为什么不能实例化泛型类型数组?

        解答:在 Java 中,泛型数组的实例化是不允许的,这是因为 Java 的泛型是通过类型擦除实现的。类型擦除会将泛型信息在编译时擦除掉,所以在运行时,Java 虚拟机无法识别泛型类型。因此,你不能直接实例化泛型类型的数组。例如,如果你尝试这样做:

T[] array = new T[10];

        在编译时,T 会被擦除为它的上限类型(如果没有明确指定上限,则默认为 Object),因此实际上会被编译器替换为: 

Object[] array = new Object[10];

        这样的语句是合法的,但是如果 T 有其他上限类型,它将会被替换为该上限类型。因此,编译器无法直接创建一个泛型类型的数组。

        要解决这个问题,可以使用集合类来代替数组,例如 ArrayList,它可以容纳泛型类型的元素。


 四.泛型的上界

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

4.1 语法 

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

4.2 示例 

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

4.3 复杂示例 

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

E必须是实现了Comparable接口的  


 五.泛型方法

5.1 定义 

          当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数

5.2 语法 

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

5.3 示例 

public class Util {
//静态的泛型方法 需要在static后用<>声明泛型类型参数
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}

5.4 将静态方法声明为泛型方法

        之前讲到过,我们在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。 

public class Test<T> {   
	// 泛型类定义的类型参数 T 不能在静态方法中使用
	// 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
    public static <E> E show(E one) {     
        return null;    
    }    
}

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

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

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

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

        以上就是对泛型相关知识的一些介绍,如果有感兴趣的同学可以多花点心思钻研,毕竟学无止境嘛,相信大家都能够学有所成!

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值