数据结构前置知识(下)

1. 包装类

Java为了让基本数据类型也能够继承Object,因此给每个基本数据类型提供了包装类,

这样就可以和平常的引用数据类型一样使用了,并且也可以应用在泛型上(后续讲)

|------------|-----------|
| 基本数据类型 | 包装类 |
| byte | Byte |
| short | Short |
| int | Interger |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |

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

1.1 装箱和拆箱

装包,装箱: 把基本数据类型给到引用数据类型

// 装包: 把基本数据类型给到引用数据类型
 public static void main(String[] args) {
        Integer a = 10;
        int i = 99;
        Integer b = i;//这俩个都是自动装包,隐式装包
        System.out.println(a);
        System.out.println(b);
        Integer aa = Integer.valueOf(10);//这个是显示装包,装箱      
    }
//
10
99

Interger a = 10; a是Interger引用数据类型

int i = 99; i是基本数据类型

Integer b = i; 我们把基本数据类型变量放在引用数据类型变量里面

以上完成了隐式装包操作

Integer aa = Integer.valueOf(10); 这个是显示装包

总结:

引用数据类型 变量名 = 基本数据类型变量/传数值; 隐式装包

引用数据类型 变量名 = 引用数据类型.valueOf(基本数据类型变量/传数值); 显示装包

拆箱,拆包: 把引用数据类型给到基本数据类型

    public static void main(String[] args) {
        //拆箱
        Integer a = 10;//隐式拆箱
        int i = a ;//把引用数据类型->基本数据类型(一般俩种都用到valueOf方法)
        System.out.println(i);
        //显示拆箱
        int aa = a.intValue();
        double d = a.doubleValue();//显示拆箱
        System.out.println(aa);
        System.out.println(d);
    }
//
10
10
10.0

Integer a = 10; a是引用数据类型

int i = a ; 这个是引用数据类型变量赋值给基本数据类型,完成了拆包过程

上面是隐式拆包

int aa = a.intValue(); 这个是显示拆包

总:

基本数据类型 变量名 = 引用数据类型变量名; 隐式拆包

基本数据类型 变量名 = 引用数据类型变量名.基本数据类型Value() 显示拆包

再来看看这个代码

    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        System.out.println(a == b);//[-128,127]
        Integer a1 = 200;
        Integer b1 = 200;//源码里面,如果超过127则会new一个新的对象
        System.out.println(a1 == b1);
    }
//
true
false

从这个代码我们可以看出,引用数据类型一旦超过了它所表示的范围就会new一个新的对象,比如上面这个例子Integer的范围是[-128,127],因此200超过了它的范围,会new出一个新的对象

2. 泛型

所谓泛型就是适用于很多类型 ,从代码来说就是对类型实现了参数化

2.1 引出泛型

我们来看这么一个例子:实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?

class Myarray1 {
    //TODO 这个是尝试的法一
    public Object[] array = new Object[10];
    //存放数据
    public void setValue(int pos,Object val) {
        array[pos] = val;
    }
    //获取数据
    public Object getValue(int pos) {
        return array[pos];
    }
}

既然这个数组可以存放任何类型,那么我们就可以New一个Object类型的数组,我们又需要存放和得到数组里面的 数据,那么我们就要为数组提供set和get方法.这样写虽然感觉没问题,但是当我们在main里面创建这个对象并且调用set和get方法的时候就出现了问题

因为getValue返回的类型是个Object的类型,我们不能直接把父类类型放在子类类型里面,因此我们需要向下转型:

复制代码
String str = (String) myarray.getValue(1);

但是,如果每次放进去什么数据,拿出来的时候我们还需要对它进行对应类型的向下转型就太过于麻烦了.所以我们就引出了泛型,把数据类型当成参数传过去,就完成了数据类型不匹配的问题.我们来看以下代码:

class Myarray<T> {//<T>表示当前的类是个泛型类,它只是一个占位符
    //TODO 这个是法二,用的泛型
    //泛型表示我可以放进去的类型是我自己指定的类型
    public Object[] array = new Object[10];
//    public T[] a = new T[10];//泛型是编译时期存在的,当程序运行起来到JVM之后,就没有泛型的概念了,而对于数组而言,创建出来就需要是一个确定的类型
    //而且,我们无法确保Object类型有没有无参的构造方法
    //泛型在编译的时候是通过擦除机制,擦成了Object
    //,2:54:14
    public void setValue(int pos,T val) {
        array[pos] = val;
    }
    //获取数据
    public T getValue(int pos) {
        return (T)array[pos];//把返回的类型,强转为指定类型
    }

}

我们来分析以下这个代码

class Myarray<T> 这里面的<T>表示当前的类是个泛型类,它只是一个占位符

然后里面所有牵扯到值的类型的时候,我们都用这个占位符表示

return (T)array[pos];这个代码就是把Object类型的数组类型的强转为指定类型的值,指定类型就是在main函数里面 <Integer>这里面的类型.然后我们可以看出,在后续我们getValue的时候就不需要强转了,就相当于在创建这个对象的时候我们已经从<Integer>这里晓得了这个就是个Integer类型的对象.我们需要传入什么数据类型的值,我们就创建对象的时候<>在这里面告诉编译器,我们要的是什么类型的数据,后续如果放入其他类型就会报错了.

所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型

2.2 泛型的语法

class 泛型名称 <类型参数列表> { }

class 泛型名称<T1,T2,T3...>

class 泛型名称<类型参数列表>extends 继承类 { }

class 泛型名称<T1,T2,T3...>extends parentClass<T>{}

其中,<T>代表占位符,表示当前的类是一个泛型类

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

E 表示 Element

K 表示 Key

V 表示 Value

N 表示 Number T 表示 Type

S, U, V 等等 - 第二、第三、第四个类型

泛型类的使用:

泛型类名<引用数据类型> 对象名 = new 泛型类名<引用数据类型>();

2.3 泛型的上界

我们通过一个例子来说明一下

class Person {

}
class Stundent extends Person  {

}
//TODO T 一定是Number 或者Number的子类
class  TestGneric <T extends Number> {

}
//一定是Person或者Person的子类
class TestGneric2 <T extends Person> {

}
public class Test2 {
    public static void main(String[] args) {
        //TODO 泛型的上界
        TestGneric<Number> testGneric = new TestGneric<>();
        TestGneric<Integer> testGneric1 = new TestGneric<>();
//        TestGneric<String> testGneric2 = new TestGneric<String>();
        TestGneric2<Stundent> testGneric2 = new TestGneric2<>();
//一定是Person或者Person的子类
        TestGneric2<Person> testGneric21 = new TestGneric2<>();
    }
}

class TestGneric <T extends Number>{}这个类表示的泛型类, T一定是Number或者Number的子类

这个就说明了我们可以通过extends关键字,限制我们使用的泛型的类型,我们在main里面创建它的对象,我们在<>里面的的类型只能是Number或者Number的子类,我们不能放其他引用类型.

然后我们自己实现一个父子类关系来实验,Student继承自Person类,我们限制TestGneric2的泛型类型只能是Person或者是Person的子类 class TestGneric2 <T extends Person>;

不然就会报错.

2.4 泛型方法

既然我们学会了写泛型类,那么我们也要学会写泛型方法

我们先看语法:

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

看个例子:

复制代码
写一个泛型类,求一个数组中的最大值

关于这个代码为什么报错,可以去看看我java分类,类和对象抽象类那一块,我简单解释一下,我们引用数据类型是必须是实现comparable接口才能够指定对什么进行比较,不能够直接进行比较.

因此我们需要进行修改

class Alg2<T extends Comparable<T>> {//把指定的类型放在类上
    public T findMaxValue(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;
    }
}

在这个代码中,我们把Alg2限制为一个实现了Compareable接口的类或者其子类上

我们来观察一下Integer的源码:

它是实现了Comparable接口的

Comparable本身是个泛型接口

因此上面代码可以找出最大值:8

但是我们任然用的是泛型类,下面我们来用法二

class Alg3 {
    public<T extends Comparable<T>> T findMaxValue(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;
    }
}

这个玩意就是我们在普通类里面写一个泛型方法我们限定T是要实现Comparable 接口的类型

访问修饰限定符<T extends 接口<T>> T 方法名 (T 参数) {

}

我们在main里面创建 这个对象,并且创建你想比较的实现了Comparable接口的引用类型数组.然后我们把数组当作参数传入方法里面进行计算.我们根据传进去实参的传值来推导此时的类型,缩写:System.out.println(alg3.findMaxValue(integers));//这个直接根据你传进去的参数来预判出是什么类型,完整写法System.out.println(alg3.<Integer>findMaxValue(integers));

我们写一个不需要实例化对象的静态方法

class Alg4 {
    public static <T extends Comparable<T>> T findMaxValue(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 Test {
        public static void main(String[] args) {
            Integer[] integers = {1,2,3,4,6,8};

            System.out.println(Alg4.findMaxValue(integers));
            System.out.println(Alg4.<Integer>findMaxValue(integers));
        }
}

静态的方法,我们直接用对象名来进行调用即可,其他用法和上述一样.

2.5 擦除机制

我们学了这么多泛型的东西,那么我们来聊一下它底层是怎么实现的,

通过命令:javap -c 查看字节码文件,所有的T都是Object

在编译过程中,我们把所有的T替换成Object,这种机制就是擦除机制

然后,我们提出一个问题,我们在创建泛型的时候我们创建的类型为什么是Object类型的,为什么不能直接用泛型创建数组

因为泛型是通过类型擦除实现的,这意味着在运行时候泛型的具体类型会被擦除,编译器无法确定T的具体数据类型,而创建数组,我们是需要知道具体创建类型是什么的.具体硬是要实例化泛型数组就要用到反射机制了(自行了解)

相关推荐
eternal__day7 小时前
数据结构十大排序之(冒泡,快排,并归)
java·数据结构·算法
阿华的代码王国9 小时前
【算法】栈
数据结构·算法·leetcode·
SpongeG9 小时前
数据结构_平衡二叉树
数据结构·算法
shentuyu木木木(森)9 小时前
入门STL(map/multiset)
开发语言·数据结构·c++·算法·map·multiset
Psycho_MrZhang10 小时前
常见的数据结构和应用场景
数据结构·算法·哈希算法
SRKkAI11 小时前
取子串(指针)
数据结构·c++·算法
zyh_03052111 小时前
GO--堆(have TODO)
数据结构·算法·golang
十一292812 小时前
数据结构--链表和双向链表的详解及实现(哨兵位详解 数组和链表的区别)
数据结构·链表
苓诣13 小时前
寻找重复数
数据结构·算法·leetcode
Cooloooo14 小时前
完全二叉树【东北大学oj数据结构9-1】C++
数据结构