【Java集合与数据结构】篇2 泛型与包装类基础

本文深入探讨了Java中的泛型和包装类。泛型提供了一种在编译时期进行类型检查的方式,避免了运行时的强制类型转换,增强了代码的可读性和安全性。包装类解决了基本数据类型无法作为对象使用的难题,通过自动装箱和拆箱提供了与对象交互的便利。此外,文章还通过阿里面试题解释了Integer对象相等性的特殊性。
摘要由CSDN通过智能技术生成

前言:本篇继续对数据结构知识的介绍与学习,上一篇中我们介绍到了关于Java中的数据结构,其中的数据框架等等。而本篇讲到的是有关型与包装类基础的知识,了解一下泛型以及包装类(这里是简单了解,后续有文章专门介绍)。

话不多说,直接进入主题。


一、泛型(Generic)

首先我们先不谈什么是泛型,我们先看一下这个顺序表的代码:

class MyArrayList{
    private int[] elem;
    private int useSize;
    public MyArrayList(){
        this.elem = new int[10];
    }
    public void add(int val){
        this.elem[useSize] = val;
        useSize++;
    }
    public int get(int pos){
        return this.elem[pos];
    }
}

在这里我们不讨论一些使用问题,我们在这里是想着一个问题,像下面的图片中演示的一样,虽然这里可以存放数据,但都只能存放整形数据,不能放其他的,那么我们能不能弄一个通用一点的呢?

然后我们在上一篇中找到一个Object类,如果把这个int数组改为Object的,是不是就可以实现我们的设想了呢?

变成这样子:

class MyArrayList{
    private Object[] elem;
    private int useSize;
    public MyArrayList(){
        this.elem = new Object[10];
    }
    public void add(Object val){
        this.elem[useSize] = val;
        useSize++;
    }
    public Object get(int pos){
        return this.elem[pos];
    }
}

然后我们再去尝试的把上面的代码搬下来,运行是通过了的,所以我们可以放字符串,也可以放数字了,但是这时候我们又有新的问题了,存放元素的时候,没有指定的,什么都可以放,那么我们取的时候,是不是也可以取出来呢,我们试试:

    public static void main(String[] args) {
        MyArrayList myArrayList = new MyArrayList();
        myArrayList.add(10);
        myArrayList.add("kkk");
        String ret = myArrayList.get(1);
    }

我们看到,存进去的时候是可以存的,但是取的时候不能直接取了,因为是Object的类型,所以我们得强制类型转换一下:

    public static void main(String[] args) {
        MyArrayList myArrayList = new MyArrayList();
        myArrayList.add(10);
        myArrayList.add("kkk");
        String ret = (String)myArrayList.get(1);
        System.out.println(ret);
    }

那我在这里先是用Object去存入又要强制类型转换取出,不就是脱裤子放屁,多此一举吗?所以这里我们产生了新的疑问:

  1. 能不能我们指定这个顺序表的类型?
  2. 指定类型之后,是不是就只能放指定类型的数据了?
  3. 取出数据的时候,能不能不要进行转换?

有!这时候就轮到我们的泛型登场了!

我们先来看看泛型的样子:class MyArrayList<E>,其实就是在类后面加上了一个尖括号以及E,这个<E>代表当前类是一个泛型类,而E就是一个占位符。

然后当我们创建对象的时候,就可以把这个占位符换成我们想要的类型,也就是在创建对象的过程中,把类型参数化了:

 public static void main(String[] args) {
        MyArrayList<Integer> myArrayList1 = new MyArrayList();
        MyArrayList<String> myArrayList2 = new MyArrayList<>();
        MyArrayList<Boolean> myArrayList3 = new MyArrayList<>();
    }

然后我们就可以尝试一下刚刚的想法去写代码:

    public static void main(String[] args) {
        MyArrayList<Integer> myArrayList1 = new MyArrayList();//整形
        myArrayList1.add(123);
        myArrayList1.add(456);//存入整形数据
        int ret = myArrayList1.get(1);
        System.out.println(ret);//取出整形数据

        MyArrayList<String> myArrayList2 = new MyArrayList<>();//字符串
        myArrayList2.add("abc");
        myArrayList2.add("happy");//存入
        System.out.println(myArrayList2.get(1));//取出

        MyArrayList<Boolean> myArrayList3 = new MyArrayList<>();//布尔类型
        myArrayList3.add(true);
        myArrayList3.add(false);

    }

我们上面的成功了,确实是可以想存什么存什么,且取出的时候并不需要强制类型转换,然后我们再试试输入其他类型的,如图,是不能存进去的!

所以我们总结出泛型的意义:

泛型的意义:
1.自动对类型进行检查
2.自动对类型进行强制类型转换

泛型还有以下的细节:

1.泛型尖括号中的内容,不参与类型的组成

我们看到创建的泛型类都是带<E>的,但实际上在这过程中,泛型尖括号中的内容,不参与类型的组成,我们可以看看这些对象:

    public static void main(String[] args) {
        MyArrayList<Integer> myArrayList1 = new MyArrayList();
        MyArrayList<String> myArrayList2 = new MyArrayList<>();
        MyArrayList<Boolean> myArrayList3 = new MyArrayList<>();

        System.out.println(myArrayList1);
        System.out.println(myArrayList2);
        System.out.println(myArrayList3);
    }

我们可以看到的是:

也就是说这些对象还是MyArrayList的,这是为什么呢,这里在一些公司的面试过程中也会问到:泛型是怎么编译的?

其实在代码编译的过程中,泛型是编译过程中的一种机制,叫做擦除机制,也就是说,当我们进行编译的时候,尖括号和里面的内容就会被砍掉了,到运行结束时当然是没有了。

总结:

  1. 泛型是为了解决某些容器、算法等代码的通用性而引入,并且能在编译期间做类型检查。
  2. 泛型利用的是 Object 是所有类的祖先类,并且父类的引用可以指向子类对象的特定而工作。
  3. 泛型是一种编译期间的机制,即 MyArrayList<String>MyArrayList<Boolean> 在运行期间是一个类型。
  4. 泛型是 java 中的一种合法语法,标志就是尖括号 <>

二、包装类(Wrapper Class)

然后就到我们的包装类,这里我们又产生了一个问题:Object 引用可以指向任意类型的对象,但有例外出现了,8 种基本数据类型不是对象,那岂不是刚才的泛型机制要失效了?

实际上也确实如此,为了解决这个问题,java 引入了一类特殊的类,即这 8 种基本数据类型的包装类,在使用过程中,会将类似 int 这样的值包装到一个对象中去。

然后我们进一步看一下包装类中的应用。


1.基本数据类型和包装类直接的对应关系

基本数据类型和包装类直接的对应关系如下:

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

对于这些数据类型的包装类,其实我们只需要记忆两个比较特别的变换,一个是int的包装类是Integer,另一个是char的包装类是Character,其他的都是首字母大写就就是其包装类了。

当我们拥有这些包装类之后,我们就可以使用这些包装类的方法来进行一些基本数据类型做不到的操作了,比如数据类型转换:

    public static void main(String[] args) {
        String str = "12345";
        int ret = Integer.valueOf(str);//使用valueOf方法进行转换
        System.out.println(str);//12345
        System.out.println(ret+1);//12346
    }

2.包装类的使用,装箱(boxing)和拆箱(unboxing)

在学习包装类的过程中,少不了对于装箱和拆箱的了解,那么什么是装箱什么是拆箱呢,我们可以这样理解:

装箱(也叫装包):是把简单类型转换成包装类类型
拆箱(也叫拆包):是把包装类类型转换成简单数据类型

那么在代码中具体是什么样子的呢,我们来看一下:

    public static void main(String[] args) {
        Integer a = 123;//装箱  装包【隐式的】
        int b = a;//拆箱  拆包【隐式的】
        System.out.println("a="+a+" b=" + b);
        //a=123 b=123
    }

其实很好理解,就是当我们用包装类类型来定义简单类型的值时,就是装箱,而由包装类类型赋值给简单类型,就是拆箱了,而上面我们的注释中写到:隐形的装箱拆箱,所以我们也有显性的装拆:

    public static void main(String[] args) {
        Integer a2 = Integer.valueOf(123);//显性的装包
        Integer a3 = new Integer(123);//显性的装包

        int b2 = a2.intValue();//显性的拆包
        double d = a2.doubleValue();//显性的拆包
    }

简单来说,就是包装类是类,有很多方法,我们在装箱拆箱的时候就是使用这些方法去进行的,但是也可以有更简洁的写法,就是上面的隐形操作。


3.阿里面试题

上面的装箱拆箱似乎很简单?但阿里曾经也在面试的时候出过这个知识的题目,我们来看一下:

请问下面代码输出是true还是false,为什么?

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

那么你的答案是哪一个呢?

答案是:…

是false!!!

按照正常来说,int类型的话肯定是true的,然后变成包装类之后再这样问,肯定是有蹊跷了的,所以很大可能就是false了,但我们要理解背后的原因。

解析:

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

遇到问题,首先要找到出发点,这里的出发点是什么呢,没错,就是上面提到的包装类类型,既然是变成包装类类型之后出现的变化,那么问题很可能就在包装类上:

上面提到说我们显性的装包是用到了包装类的方法,所以我们就从方法入手看看,比如把上面的代码改为Integer a = Integer.valueOf(128);,然后点开看看这个方法的底层代码是什么样子的,按住ctrl用左键点击valueOf

//底层代码
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

很显然,我们看见底层代码中并不是直接把赋值存入,而是进行了一次if语句的判断执行,我们解析一下这个if语句:

首先我们看见当我们的参数进入到valueOf的方法中后,遇到了应该if判断,判断中判断了参数值是否在某个范围内,在的话就返回一个计算后的数,不在就创建新的对象。

然后我们来看一下这个范围怎么求,按住ctrl键左击这个IntegerCache,得到下面的源码:

从源码中我们要提取出我们需要的数据和信息,这里我们看到,在上面valueOf的底层代码中,我们if语句的范围是low和high之间,在这里我们找到这两个的值:low= - 128 , high = 127,然后还有一个cache,在这里我们知道是一个数组,而是这样的数组:

也就是说是一个有256个元素且从-128递增1的数组。

然后我们来再看if语句:

//底层代码
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

得到当我们输入的参数在-128到127的时候,得到直接返回的值,比如我们输入的是-128,得到的就是IntegerCache.cache[i + (-IntegerCache.low)]也就是IntegerCache.cache[-128+ 128]得到cache[0] = -128,也就是正常得出原数。

而当我们不再这个范围的时候,则需要创建一个新的对象,我们知道,创建新的对象意味着a和b并不是同一个对象,所以他们并不会相同,虽然看起来值是相同的,但是在内存中,他们是不相同的!

这就是这道阿里面试题。


这就是本篇Java集合与数据结构篇2的全部内容啦,有了泛型与包装类的基础,我们下一篇就会进入ArrayList的世界了!欢迎关注。一起学习,共同努力!也可以期待下个系列接下来的博客噢。

还有一件事:

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恒等于C

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

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

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

打赏作者

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

抵扣说明:

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

余额充值