【数据结构】 简单认识包装类与泛型

文章目录

  • 包装类
    • 基本数据类型和对应的包装类
    • 拆箱和装箱
    • 自动装箱和自动拆箱
    • 包装类面试题
    • 什么是泛型
      • 为什么要使用泛型
      • 泛型类的创建语法
      • 泛型类的使用
        • 语法
        • 示例
        • 类型推导(Type Inference)
        • 裸类型(Raw Type)
        • 泛型如何编译的
          • 擦除机制
          • 为什么不能实例化泛型类型数组
          • 泛型的上界
            • 语法
            • 示例
            • 复杂示例
            • 泛型方法
              • 定义语法
              • 示例
              • 总结

                包装类

                在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。

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

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

                拆箱和装箱

                装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型,有了装箱拆箱,java可以根据上下文,自动进行转换,极大的简化相关编程。

                自动装箱和自动拆箱

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

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

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

                当我们查看实现逻辑时,发现java已经自动帮我们实现了

                包装类面试题

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

                public class Main { public static void main(String[] args) { Integer i1 = 100;
                        Integer i2 = 100;
                        Integer i3 = 200;
                        Integer i4 = 200;
                 
                        System.out.println(i1==i2);
                        System.out.println(i3==i4);
                    }
                }
                

                答案为

                true

                false

                为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:

                public static Integer valueOf(int i) { if(i >= -128 && i <= IntegerCache.high)
                            return IntegerCache.cache[i + 128];
                        else
                           return new Integer(i);
                    }
                

                而其中IntegerCache类的实现为:

                private static class IntegerCache { static final int high;
                        static final Integer cache[];
                        static { final int low = -128;
                            // high value may be configured by property
                            int h = 127;
                            if (integerCacheHighPropValue != null) { // Use Long.decode here to avoid invoking methods that
                                // require Integer's autoboxing cache to be initialized
                                int i = Long.decode(integerCacheHighPropValue).intValue();
                                i = Math.max(i, 127);
                                // Maximum array size is Integer.MAX_VALUE
                                h = Math.min(i, Integer.MAX_VALUE - -low);
                            }
                            high = h;
                            cache = new Integer[(high - low) + 1];
                            int j = low;
                            for(int k = 0; k < cache.length; k++)
                                cache[k] = new Integer(j++);
                        }
                        private IntegerCache() {}
                    }
                

                从这两段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。

                上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象

                什么是泛型

                一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

                ----- 来源《Java编程思想》对泛型的介绍。

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

                为什么要使用泛型

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

                思路大致为:

                1. 我们以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] strs = new String[10];
                2. 所有类的父类,默认为Object类。我们将数组创建为Object;

                  ![在这里插入图片描述](https://img-blog.csdnimg.cn/3a3f22d364e24c17bb27c7f97c210b01.png

                存在问题:我们发现以上代码实现后发现

                • 任何类型数据都可以存放
                • 1号下标本身就是字符串,但是确编译报错。必须进行强制类型转换

                  虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。

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

                  泛型类的创建语法

                  class 泛型类名称<类型形参列表> {// 这里可以使用类型参数
                  }
                  class ClassName {}
                  

                  也可以实现继承,并继承里面的泛型类

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

                  那么刚刚上述代码就可以变为

                  上面代码涉及了许多新的东西,这里会一一做出解释

                  代码解释:

                  1. 类名后的 代表占位符,表示当前类是一个泛型类

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

                  E 表示 Element

                  K 表示 Key

                  V 表示 Value

                  N 表示 Number

                  T 表示 Type

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

                  1. 注释1处,不能new泛型类型的数组,意味着
                  T[] ts = new T[5];//是不对的
                  

                  上述的代码:T[] array = (T[])new Object[10];是否就足够好,答案是未必的。这块问题一会儿介绍。

                  1. 注释2处,类型后加入 指定当前类型

                  2. 注释3处,不需要进行强制类型转换

                  3. 注释4处,代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释4处,编译器会在存放元素的时候帮助我们进行类型检查。

                  泛型类的使用

                  语法

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

                  示例

                  MyArray list = new MyArray();
                  

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

                  类型推导(Type Inference)

                  当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

                  MyArray list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 Integer
                  

                  裸类型(Raw Type)

                  裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型

                  MyArray list = new MyArray();
                  

                  注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制下面的类型擦除部分,我们也会讲到编译器是如何使用裸类型的。

                  小结:

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

                  泛型如何编译的

                  擦除机制

                  那么,泛型到底是怎么编译的?通过命令:javap -c 查看字节码文件,所有的T都是Object

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

                  Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息

                  有关泛型擦除机制的文章截介绍

                  为什么不能实例化泛型类型数组

                  例如以下代码

                  原因:替换后的方法为:将Object[]分配给Integer[]引用,程序报错

                  public Object[] getArray() {return array;
                  }
                  

                  通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的

                  向下转型不安全

                  那么我们有没有正确的方式呢?这里也是有的,博主在这里给大家提供一种

                  class MyArray { public T[] array;
                      public MyArray() { }
                      / **
                      *通过反射创建,指定类型的数组
                      * @param clazz
                      * @param capacity
                      */
                      public MyArray(Class clazz, int capacity) { array = (T[])Array.newInstance(clazz, capacity);
                      }
                      public T getPos(int pos) { return this.array[pos];
                      }
                      public void setVal(int pos,T val) { this.array[pos] = val;
                      }
                      public T[] getArray() { return array;
                      }
                  }
                  public static void main(String[] args) { MyArray myArray1 = new MyArray<>(Integer.class,10);
                      Integer[] integers = myArray1.getArray();
                  }
                  

                  泛型的上界

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

                  语法

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

                  示例

                  public class MyArray {...
                  }
                  

                  只接受 Number 的子类型作为 E 的类型实参

                  MyArray l2; // 编译错误,因为 String 不是 Number 的子类型
                  MyArray l1; // 正常,因为 Integer 是 Number 的子类型
                  

                  结果为

                  error: type argument String is not within bounds of type-variable E
                  	MyArrayList l2;
                  			^
                  where E is a type-variable:
                  	E extends Number declared in class MyArrayList
                  

                  没有指定类型边界 E,可以视为 E extends Object

                  复杂示例

                  public class MyArray> {...
                  }
                  

                  E必须是实现了Comparable接口的

                  此时我们可以用这个知识对任意数组进行寻找最大值

                  class Alg> { public T findMax (T[] array) { T max = array[0];
                          for (int i = 1; i < array.length; i++) { if(max.compareTo(array[i]) < 0 ) { max = array[i];
                              }
                          }
                          return max;
                      }
                  }
                   public static void main(String[] args) { Alg alg = new Alg<>();
                          Integer[] array = {1,2,3,4};
                          Integer ret = alg.findMax(array);
                          System.out.println(ret);
                      }
                  

                  泛型方法

                  你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

                  下面是定义泛型方法的规则:

                  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
                  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
                  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
                  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

                    定义语法

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

                    示例

                    class Alg{ //泛型方法
                        public static> T findMax (T[] array) { T max = array[0];
                            for (int i = 1; i < array.length; i++) { if(max.compareTo(array[i]) < 0 ) { max = array[i];
                                }
                            }
                            return max;
                        }
                    }
                    public static void main(String[] args) { Integer[] array = {1,2,3,4};
                        Integer ret = Alg2.findMax(array);
                        System.out.println(ret);
                    }
                    

                    此处的Interger可以选择不加,java可以根据你array里面值的类型进行推导

                     Integer ret = Alg2.findMax(array);
                      Integer ret = Alg2.findMax(array);
                    

                    总结

                    关于《 【数据结构】 简单认识包装类与泛型》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!