如根据以前面向对象的知识,我们构建了一个超级数组,但是我们之前构建的那个超级数组只能存放int,不能存放其它数据,如果要存放其它数据我们只能更改源代码,将超级数组内的数组类型修改,这样直接修改我们的代码是极其不理智的一件事情。
超级数组原代码:
public class SuperArray{
private String name;
/**
* 维护一个数组
*/
private int[] superArray;
/**
* 该常量表示数组的默认长度
*/
private static final Integer DEFAULT_LENGTH = 10;
/**
* 该常量表示数组的扩容的阈值
*/
private static final Double LOAD_NUM = 0.75;
/**
* 表示当前数组已占容量
*/
private Integer currentIndex;
/**
* 表示数组总容量
*/
private Integer capacity;
public SuperArray() {
this(DEFAULT_LENGTH);
}
public SuperArray(Integer capacity) {
superArray = new int[capacity];
this.capacity = capacity;
currentIndex = -1;
}
public void judgeAround(int index){
if (index < 0 || index > currentIndex)
{
throw new RuntimeException("数组索引越界");
}
}
/**
* 该方法用于检查数组是否需要扩容
* @return
*/
public Boolean check(){
if (currentIndex + 1 >= (Integer) capacity*LOAD_NUM){
return false;
}
return true;
}
/**
* 该方法用于数组的扩容
*/
public void resize(){
int oldLen = this.capacity;
int newLen = this.capacity + this.capacity >> 1;
int[] tmp = new int[newLen];
for (int i = 0; i < size(); i++) {
tmp[i] = superArray[i];
}
superArray = tmp;
tmp = null;
}
/**
* 该方法用于判断数组是否为空
* @return
*/
public Boolean isNull(){
if (superArray == null){
return false;
}
return true;
}
public void addToHeader(int data) {
if (!isNull()){
return;
}
if (!check()){
resize();
}
for (int i = currentIndex; i > 0; i--) {
superArray[i+1] = superArray[i];
}
}
public void addToTail(int data) {
if (!isNull()){
return;
}
if (!check()){
resize();
}
superArray[++currentIndex] = data;
}
public int get(int index) {
judgeAround(index);
return (int)superArray[index];
}
public void set(int index, int data) {
judgeAround(index);
superArray[index] = data;
}
public void remove(int index) {
judgeAround(index);
for (int i = index; i < size(); i++) {
superArray[i] = superArray[i+1];
}
currentIndex--;
}
public int size() {
return currentIndex + 1;
}
@Override
public String toString() {
final StringBuilder sp = new StringBuilder();
sp.append("[");
for (int i = 0; i < size(); i++) {
sp.append(superArray[i]);
if ((i + 1) <= size() - 1){
sp.append(",");
}
}
sp.append("]");
return sp.toString();
}
}
由上述问题,我们可以进行改进一下,让我们的超级数组类可以存放任意数据类型的数组。
第一种解决方案:
将内部的int数组,换成Object类型数组,使用引用数据类型替代基础数据类型。因为Object是所有类的超类,所以任何子类都可以传递进去,我们的代码可以简化如下:
public class SuperArray{
private String name;
/**
* 维护一个数组
*/
private Object[] superArray;
/**
* 该常量表示数组的默认长度
*/
private static final Integer DEFAULT_LENGTH = 10;
/**
* 该常量表示数组的扩容的阈值
*/
private static final Double LOAD_NUM = 0.75;
/**
* 表示当前数组已占容量
*/
private Integer currentIndex;
/**
* 表示数组总容量
*/
private Integer capacity;
public SuperArray() {
this(DEFAULT_LENGTH);
}
public SuperArray(Integer capacity) {
superArray = new Object[capacity];
this.capacity = capacity;
currentIndex = -1;
}
public void judgeAround(int index){
if (index < 0 || index > currentIndex)
{
throw new RuntimeException("数组索引越界");
}
}
/**
* 该方法用于检查数组是否需要扩容
* @return
*/
public Boolean check(){
if (currentIndex + 1 >= (Integer) capacity*LOAD_NUM){
return false;
}
return true;
}
/**
* 该方法用于数组的扩容
*/
public void resize(){
int oldLen = this.capacity;
int newLen = this.capacity + this.capacity >> 1;
Object[] tmp = new Object[newLen];
for (int i = 0; i < size(); i++) {
tmp[i] = superArray[i];
}
superArray = tmp;
tmp = null;
}
/**
* 该方法用于判断数组是否为空
* @return
*/
public Boolean isNull(){
if (superArray == null){
return false;
}
return true;
}
public void addToHeader(Object data) {
if (!isNull()){
return;
}
if (!check()){
resize();
}
for (int i = currentIndex; i > 0; i--) {
superArray[i+1] = superArray[i];
}
}
public void addToTail(Object data) {
if (!isNull()){
return;
}
if (!check()){
resize();
}
superArray[++currentIndex] = data;
}
public int get(int index) {
judgeAround(index);
return (int)superArray[index];
}
public void set(int index, Object data) {
judgeAround(index);
superArray[index] = data;
}
public void remove(int index) {
judgeAround(index);
for (int i = index; i < size(); i++) {
superArray[i] = superArray[i+1];
}
currentIndex--;
}
public int size() {
return currentIndex + 1;
}
@Override
public String toString() {
final StringBuilder sp = new StringBuilder();
sp.append("[");
for (int i = 0; i < size(); i++) {
sp.append(superArray[i]);
if ((i + 1) <= size() - 1){
sp.append(",");
}
}
sp.append("]");
return sp.toString();
}
}
但是这样又引发了另一个问题,那就是我们的超级数组里面,每new一个超级数组,这个超级数组什么数据都能往里面塞,里面可以存int,可以存double,可以存我们自己定义的类,一个数组里面的数据的类型可以是任意类型,但是这样就和我们数组的概念相驳了。
我们规定传入的对象只要是Object子类就行,那就意味着,所有的对象都可以往篮子里扔,可以扔水 果,也可以扔炸弹,数据类型不能很好的统一。
从超级数组中获取数据后必须强转才能使用,这是不是意味着,极有可能发生ClassCastException。
SuperArray superArray = new SuperArray();
superArray.add(new Date());
superArray.add(new Dog());
(Dog)superArray.get(0);
那怎么解决这个问题呢? 我们的目标是:
【泛型】就能够很好的解决这个问题。
public interface Super<T> {
/**
* 该方法用于头插法
* @param data 待插入数据
*/
void addToHeader(T data);
/**
* 该方法用于尾插法
* @param data 待插入数据
*/
void addToTail(T data);
/**
* 该方法用于根据索引,返回元素
* @param index
* @return
*/
T get(int index);
/**
* 该方法用于修改元素
* @param index
* @param t
*/
void set(int index,T t);
/**
* 该方法用于根据索引,删除元素
* @param index
*/
void remove(int index);
/**
* 该方法获取数组当前已占容量
* @return 返回实际已占容量
*/
int size();
}
public class SuperArray<T> implements Super<T>{
private String name;
/**
* 维护一个数组
*/
private Object[] superArray;
/**
* 该常量表示数组的默认长度
*/
private static final Integer DEFAULT_LENGTH = 10;
/**
* 该常量表示数组的扩容的阈值
*/
private static final Double LOAD_NUM = 0.75;
/**
* 表示当前数组已占容量
*/
private Integer currentIndex;
/**
* 表示数组总容量
*/
private Integer capacity;
public SuperArray() {
this(DEFAULT_LENGTH);
}
public SuperArray(Integer capacity) {
superArray = new Object[capacity];
this.capacity = capacity;
currentIndex = -1;
}
public void judgeAround(int index){
if (index < 0 || index > currentIndex)
{
throw new RuntimeException("数组索引越界");
}
}
/**
* 该方法用于检查数组是否需要扩容
* @return
*/
public Boolean check(){
if (currentIndex + 1 >= (Integer) capacity*LOAD_NUM){
return false;
}
return true;
}
/**
* 该方法用于数组的扩容
*/
public void resize(){
Integer oldLen = this.capacity;
Integer newLen = this.capacity + this.capacity >> 1;
Object[] tmp = new Object[newLen];
for (int i = 0; i < size(); i++) {
tmp[i] = superArray[i];
}
superArray = tmp;
tmp = null;
}
/**
* 该方法用于判断数组是否为空
* @return
*/
public Boolean isNull(){
if (superArray == null){
return false;
}
return true;
}
@Override
public void addToHeader(T data) {
if (!isNull()){
return;
}
if (!check()){
resize();
}
for (int i = currentIndex; i > 0; i--) {
superArray[i+1] = superArray[i];
}
}
@Override
public void addToTail(T data) {
if (!isNull()){
return;
}
if (!check()){
resize();
}
superArray[++currentIndex] = data;
}
@Override
public T get(int index) {
judgeAround(index);
return (T)superArray[index];
}
@Override
public void set(int index, T data) {
judgeAround(index);
superArray[index] = data;
}
@Override
public void remove(int index) {
judgeAround(index);
for (int i = index; i < size(); i++) {
superArray[i] = superArray[i+1];
}
currentIndex--;
}
@Override
public int size() {
return currentIndex + 1;
}
@Override
public String toString() {
final StringBuilder sp = new StringBuilder();
sp.append("[");
for (int i = 0; i < size(); i++) {
sp.append(superArray[i]);
if ((i + 1) <= size() - 1){
sp.append(",");
}
}
sp.append("]");
return sp.toString();
}
}
public class DoubleSuperArray extends SuperArray<Double>{
public DoubleSuperArray() {
super();
}
public DoubleSuperArray(Integer capacity) {
super(capacity);
}
@Override
public void judgeAround(int index) {
super.judgeAround(index);
}
@Override
public Boolean check() {
return super.check();
}
@Override
public void resize() {
super.resize();
}
@Override
public Boolean isNull() {
return super.isNull();
}
@Override
public void addToHeader(Double data) {
super.addToHeader(data);
}
@Override
public void addToTail(Double data) {
super.addToTail(data);
}
@Override
public Double get(int index) {
return super.get(index);
}
@Override
public void set(int index, Double data) {
super.set(index, data);
}
@Override
public void remove(int index) {
super.remove(index);
}
@Override
public int size() {
return super.size();
}
@Override
public String toString() {
return super.toString();
}
}
这样我们每次只需定义一个类继承自SuperArray即可,重写父类的方法,在继承声明泛型的具体类型,如下,如果我们的DoubleSuperArray,存放的不是Double类型的数据,那就直接报错了。
什么是泛型? 泛型就是能够帮助我们把【类型明确】的工作推迟到创建对象或调用方法的时候。
意思就是:我定义类的时候不用管到底是什么类型,new这个对象或者调用这个对象的方法时才确定具体的类型。
泛型主要实现在类、方法上->泛型类、泛型方法
(1)泛型类
public class SuperArray<T> implements Super<T>
(2)泛型方法
public <T> T add(T data)
(3)泛型通配符
1.无界
就是和我们上述解决方法的第一种办法一样,可以传入任意的数据类型,没有边界
2.上界
我们可以使用SuperArray superArray
的形式来约定传入参数B的上界,意思,就是传入的参数只能是B或者B的子类。
3.下界
我们可以使用SuperArray superArray
的形式来约定传入参数B的下界,意思,就是传入的参数只能是B或者B的超类。
(4)泛型擦除
泛型擦除就是我们的代码在编译时,所有的泛型信息都会被擦掉,Java的泛型基本上都是在编译器这个层次 上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,替换成Object这个过程成为类型擦除。也就是说我们有一个泛型SuperArray
所以我们引出了几个具体的问题:
1、泛型不能是基本数据类型,就比如,没有 SuperArray ,只有 SuperArray 。因为当类型擦除后, SuperArray 的原始类型变为 Object ,但是 Object 类型 不能存储 double 值,只能引用 Double 的值。
2、重载方法和泛型
public static void print(SuperArray<Double> superArray){
}
public static void print(SuperArray<Integer> superArray){
}
因为泛型被擦除后,都变成了SuperArray
,所以两个方法变成了一样的,不能构成泛型
3、多态和泛型擦除
现在有这样一个泛型类:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然后我们想要一个子类继承它。
package com.ydlclass;
import java.util.Date;
public class DatePair extends Pair<Date>{
@Override
public Date getValue() {
return super.getValue();
}
@Override
public void setValue(Date value) {
super.setValue(value);
}
}
在这个子类中,我们设定父类的泛型类型为Pair
,在子类中,我们重写了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date
,那么父类里面的两个方法的参数都为Date
类型。
所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override
标签中也可以看到,一点问题也没有,实际上是这样的吗?
分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object
,所以父类编译之后会变成下面的样子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
{
public com.ydlclass.Pair();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ydlclass/Pair;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ydlclass/Pair;
public T getValue();
// 这里我们知道这个方法的返回值是Object
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field value:Ljava/lang/Object;
4: areturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ydlclass/Pair;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ydlclass/Pair;
Signature: #20 // ()TT;
public void setValue(T);
// 这里我们知道这个方法的参数是引用数据类型,Object
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field value:Ljava/lang/Object;
5: return
LineNumberTable:
line 9: 0
line 10: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/ydlclass/Pair;
0 6 1 value Ljava/lang/Object;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 6 0 this Lcom/ydlclass/Pair;
0 6 1 value TT;
Signature: #23 // (TT;)V
}
Signature: #24 // Ljava/lang/Object;
SourceFile: "Pair.java"
再看子类的两个重写的方法的类型:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
{
public java.util.Date getValue();
descriptor: ()Ljava/util/Date;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method com/ydlclass/Pair.getValue:()Ljava/lang/Object;
4: checkcast #3 // class java/util/Date
7: areturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/ydlclass/DatePair;
public void setValue(java.util.Date);
descriptor: (Ljava/util/Date;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #4 // Method com/ydlclass/Pair.setValue:(Ljava/lang/Object;)V
5: return
LineNumberTable:
line 13: 0
line 14: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/ydlclass/DatePair;
0 6 1 value Ljava/util/Date;
// 桥接方法,一会分析
public void setValue(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/util/Date
5: invokevirtual #5 // Method setValue:(Ljava/util/Date;)V
8: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/ydlclass/DatePair;
public java.lang.Object getValue();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #6 // Method getValue:()Ljava/util/Date;
4: areturn
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ydlclass/DatePair;
}
Signature: #29 // Lcom/ydlclass/Pair;
SourceFile: "DatePair.java"
先来分析setValue
方法,父类的类型是Object
,而子类的类型是Date
,参数类型不一样,这如果是在普通的继承关系中,根本就不会是重写,而是重载。
我们在一个main方法测试一下:
public static void main(String[] args) throws ClassNotFoundException {
DatePair DatePair = new DatePair();
DatePair.setValue(new Date());
DatePair.setValue(new Object()); //编译错误
}
如果是重载,那么子类中两个setValue
方法,一个是参数Object
类型,一个是Date
类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,确实是重写了,而不是重载了。
关键字:ACC_BRIDGE, ACC_SYNTHETIC
从编译的结果来看,我们本意重写setValue
和getValue
方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的【桥方法】,我们从字节码中看到两个标志【ACC_BRIDGE, ACC_SYNTHETIC】。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在我们自己定义的setvalue
和getValue
方法上面的@Override
只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
并且,还有一点也许会有疑问,子类中的桥方法Object getValue()
和Date getValue()
是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分辨这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟机去区别。
该段落学习引用至哔哩哔哩IT楠老师
4、泛型和静态
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
举例说明:
public class Test<T> {
public static T name; //编译错误
public static T printName(T name){ //编译错误
return null;
}
}
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
但是要注意区分下面的一种情况:
public class Test<T> {
public static <T> T show(T one){ //这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。