【java数据结构】泛型
- 一、包装类
- 1.1 基本数据类型对应的包装类
- 1.2 装箱和拆箱
- 二、泛型
- 2.1 引出泛型
- 2.2 什么是泛型
- 2.3 语法
- 2.3.1 泛型类
- 2.3.2 泛型接口
- 2.3.3 泛型方法
- 2.4 擦除机制
- 2.5 泛型通配符
- 2.5.1 <?>无限定的通配符
- 2.5.2 <? extends T>上界的通配符
- 2.5.3 <? super T>下界的通配符
此篇博客希望对你有所帮助(帮助里了解包装类,以及自动拆装箱,更重要的了解java的泛型),不懂的或有错误的也可在评论区留言,错误必改评论必回!!!
一、包装类
在Java中,包装类(Wrapper Classes)是一种特殊的类,它们将基本数据类型封装成对象。这样做的目的主要是为了让基本数据类型拥有对象的特性。
1.1 基本数据类型对应的包装类
1.2 装箱和拆箱
装箱:是基本数据类型自动转换为包装类对象;拆箱:是包装类对象自动转换为基本数据类型。
从Java 5(Java 1.5)开始,引入了自动装箱(Autoboxing)和拆箱(Unboxing)机制。
int i=100;
Integer a=Integer.valueOf(i);//手动装箱
Integer b=100;//自动装箱
int c=a.intValue();//手动拆箱
int d=b;//自动拆箱
拆箱和装箱还提供额外的功能:
// 将字符串转换为整数
int number = Integer.parseInt("123");
// 将整数转换为字符串
String strNumber = String.valueOf(123);
// 也可以利用包装类的valueOf方法
String anotherStrNumber = Integer.valueOf(123).toString();
注意事项:缓存:Java的某些包装类(如Integer、Boolean等)对特定范围内的值进行了缓存,这意味着在特定范围内,使用valueOf方法得到的可能是缓存中的对象,而不是新创建的对象。这种机制提高了性能,但也需要注意它可能带来的副作用。
示例:
Integer a=100;
Integer b=100;
System.out.println(a == b);//输出结果:true
Integer c=200;
Integer d=200;
System.out.println(c == d);//输出结果:false
解释:由于100在-128到127的缓存范围内,Java虚拟机(JVM)会直接从缓存中返回相同的Integer对象给a和b。因此,a和b实际上引用的是内存中的同一个对象;200它不在-128到127的缓存范围内。因此,每次创建Integer对象时(通过自动装箱),JVM都会创建一个新的Integer实例。所以,c和d虽然值相同,但它们分别引用了不同的对象。
二、泛型
在java继承那学习到,Object为所有类的父类,那么数组是否可以创建成Object类?如果能创建成功,是不是这个数组里面能放任何类型的数据?
答案:数组可以创建成Object类,并且里面可以存放任何类型的数据。
2.1 引出泛型
class Test{
public Object[] array=new Object[10];
public Object getPos(int pos){
return array[pos];
}
public void setValues(int pos,Object value){
array[pos]=value;
}
}
public class Main {
public static void main(String[] args) {
Test test=new Test();
test.setValues(1,10);
test.setValues(2, "hello");
String str= (String) test.getPos(2);
int a= (int)test.getPos(1);
System.out.println(a);
System.out.println(str);
}
因为数组是Object类型的,当我们要用到这些数据的时候,还需要我们进行强转。
弊端:当我们创建Object数组,要用到数组中的数据时,我们还得事先知道数组这个下表的数据类型是什么类型,才可以使用。
2.2 什么是泛型
概念:泛型本质即“参数化类型”,也就是说所操作的数据类型被指定为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
主要目的:泛型就是指定当前的容器,要持有什么类型的对象,让编译器去做检查。
2.3 语法
2.3.1 泛型类
class 泛型类名称<类型形参列表>{
...
}
尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。
泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
在泛型类中,类型参数定义的位置有三处,分别为:
1.非静态的成员属性类型
2.非静态方法的形参类型(包括非静态成员方法和构造器)
3.非静态的成员方法的返回值类型
在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。
class Test<T> {
public Object[] array = new Object[10];
public T getPos(int pos) {
return (T) array[pos];
}
public void setValues(int pos, T value) {
array[pos] = value;
}
}
public class Main {
public static void main(String[] args) {
Test<Integer> test = new Test<Integer>();
test.setValues(1, 10);
test.setValues(2, "hello");
String str = (String) test.getPos(2);
int a = (int) test.getPos(1);
System.out.println(a);
System.out.println(str);
}
}
这里就发现当我们传入Integer数据类型后,再插入String类型的数据,编译器就会报错!
Test<Integer> test = new Test<>();
这里可以不用写后面<>中的Integer,这个会根据前面<>中写的包装类自动补充这里的类型!
注意:
泛型类不只接受一个类型参数,它还可以接受多个类型参数!
class Test<T,E> {
public Object[] array = new Object[10];
public T getPos(int pos) {
return (T) array[pos];
}
public void setVal(int pos, E value1) {
array[pos] = value1;
}
public void setValues(int pos, T value) {
array[pos] = value;
}
}
2.3.2 泛型接口
它允许你在定义接口时指定一个或多个类型参数(也称为类型变量)。这样,当实现接口时,你就可以指定这些类型参数的具体类型了。泛型接口提高了代码的复用性、类型安全性和可读性。
public interface 接口名<类型参数> {
...
}
示例:
// 定义一个泛型接口Pair,它有两个类型参数T和U
public interface Pair<T, U> {
// 定义一个方法,该方法返回第一个元素
T getFirst();
// 定义一个方法,该方法返回第二个元素
U getSecond();
}
// 实现Pair接口的一个类,具体指定了类型参数为String和Integer
public class StringIntPair implements Pair<String, Integer> {
private String first;
private Integer second;
public StringIntPair(String first, Integer second) {
this.first = first;
this.second = second;
}
@Override
public String getFirst() {
return first;
}
@Override
public Integer getSecond() {
return second;
}
}
// 测试类
public class Test1 {
public static void main(String[] args) {
StringIntPair StringIntPair = new StringIntPair("Hello", 123);
// Pair<String, Integer> StringIntPair = new StringIntPair("Hello", 123);
System.out.println(StringIntPair.getFirst() + ", " + StringIntPair.getSecond());
}
}
运行结果:
2.3.3 泛型方法
当在一个方法签名(方法签名主要由方法名称和参数列表(包括参数的类型和顺序)组成,但不包括方法的返回类型、访问修饰符和抛出的异常)中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}
(1)只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
class Test2<U> {
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(U u){
System.out.println(u);
}
// <T> 真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
}
(2)泛型方法中可以同时声明多个类型参数。
public class TestMethod<U> {
public <T, S> T testMethod(T t, S s) {
return null;
}
}
(3)泛型方法中也可以使用泛型类中定义的泛型参数。
public class TestMethod<U> {
public <T> U testMethod(T t, U u) {
return u;
}
}
(4)特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。泛型方法始终以自己声明的类型参数为准。
public class Test<T> {
public void testMethod(T t) {
System.out.println(t);
}
public <T> T testMethod1(T t) {
return t;
}
}
前面在泛型类的定义中提到,在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。
public class Test2<T> {
// 泛型类定义的类型参数 T 不能在静态方法中使用
// 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
public static <E> E show(E one) {
return null;
}
}
示例:
public class GenericMethods {
// 这是一个泛型方法,它接受一个泛型参数T
public static <T> void printArray(T[] inputArray) {
// 显示数组元素
for (T element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
public static void main(String args[]) {
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = {1, 2, 3, 4, 5};
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4};
Character[] charArray = {'H', 'E', 'L', 'L', 'O'};
System.out.println("整型数组元素为:");
printArray(intArray); // 传递一个整型数组
System.out.println("\n双精度型数组元素为:");
printArray(doubleArray); // 传递一个双精度型数组
System.out.println("\n字符型数组元素为:");
printArray(charArray); // 传递一个字符型数组
}
}
运行结果:
2.4 擦除机制
Java 5之前是没有泛型概念的,为了保持与旧版本的兼容性,Java引入了泛型擦除机制。
编译时期,通过<>中的T进行类型检查和类型转换;编译完成以后T被擦除为Object类型。
class Test<T> {
public Object[] array = new Object[10];
public T getPos(int pos) {
return (T) array[pos];
}
public void setValues(int pos, T value) {
array[pos] = value;
}
}
public class Main {
public static void main(String[] args) {
Test<Integer> test1=new Test<>();
Test<String> test2=new Test<>();
System.out.println(test1);
System.out.println(test2);
}
预期的运行结果:
System.out.println(test1);//预期运行结果:Test<Integer>......
System.out.println(test2);//预期运行结果:Test<String>......
实际运行结果:
2.5 泛型通配符
在编码中, 处理未知类型或需要限制类型范围时。因此引出了泛型通配符这个概念。
泛型通配符有 3 种形式:
- <?> :被称作无限定的通配符。
- <? extends T> :被称作有上界的通配符。
- <? super T> :被称作有下界的通配符。
2.5.1 <?>无限定的通配符
如字面意思一样,没有任何限制,当你想表达“任何类型都可以”时,可以使用无界通配符。但是,它限制了你可以对该类型进行的操作,因为你不能往这个集合中添加元素(除了 null),因为编译器不知道具体的类型是什么。
List<?> list = new ArrayList<String>();
// list.add(new Object()); // 编译错误
list.add(null); // 允许
注意:Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList<?> 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?> 是 ArrayList< Object > 逻辑上的父类。
2.5.2 <? extends T>上界的通配符
上界通配符 <? extends T>:T 代表了类型参数的上界,<? extends T>表示类型参数的范围是 T 和 T 的子类。
语法:
class 泛型类名称 <类型参数 extends 类型边界>{
...
}
示例:
class Test1<T extends Number> {
public Object[] array = new Object[10];
public T getPos(int pos) {
return (T) array[pos];
}
public void setValues(int pos, T value) {
array[pos] = value;
}
}
public class Test {
public static void main(String[] args) {
Test1<Integer> test1=new Test1<>();
//Test1<String> test2=new Test1<String>();//类型参数java.lang.String不在类型变量T的范围内
test1.setValues(1,10);//编译成功
}
}
原因:T继承的是Number,Number就是这个集合的上界,Integer是Number的子类,String不是Number的子类。
总结:可以读,不能写因为有上界,所以你读取的时候,里面的数据不会超过你数据类型的上界;但是在写入时,你不直达他具体的数据类型,所以不能写。
2.5.3 <? super T>下界的通配符
表示可以是 T 类型或 T 类型的父类型,适用于只写或读写但写入元素类型固定的场景。
class Test1<Number> {
public Object[] array = new Object[10];
public Number getPos(int pos) {
return (Number) array[pos];
}
public void setValues(int pos, Number value) {
array[pos] = value;
}
}
public class Test {
public static void main(String[] args) {
Test1<? super Number> test1=new Test1<>();
test1.setValues(1,10.2);//编译成功
test1.setValues(2,1);//编译成功
}
总结:可以写,不能读因为他有下界,所以可以写入下界数据类型的数据,但是读的时候你不知道他具体是什么类型。
List<? super Integer> list = new ArrayList<>();
// 编译失败,因为无法确定是否是Integer,有可能是Number
Integer i = list.get(0);
// 编译失败,因为无法确定是否是Number,有可能是Object
Number m = list.get(1);
// 编译正确,Object可以接受任意值
Object o = list.get(2);