本系列是关于数据结构与算法内容,系列内容出自“数据结构与算法分析-Java语言描述”,主要是对于书中内容的归纳总结,并将自己的一些理解记录下来,供以后翻阅。如果文章内容有误,欢迎各位批评指正。
一、定理
1、指数
X^a*X^b=X^a+b
X^a/X^b=X^a-b
(X^a)^b=X^a*b
X^n+X^n=2*X^n
2^n+2^n=2^n+1
2、对数
在计算机科学中,除非特殊说明,否则所有的对数都是以2为底的
X^a=b == logX b = a
loga^b = logx^b/logx^a
logAB=logA+logB
3、模运算
如果N整除A-B,那么就说A与B模N同余。直观地看,这意味着无论是A或者B去除以N,所得的余数都是相同的。记A≡B(mod N),同时符合A+C≡B+C(mod N),AD≡BD(mod N)
二、关于数据的一些描述与定义内容
1、数组类型的兼容性
这里我们需要了解协变、逆变与不变的性质
协变与逆变是用来描述类型转换后的继承关系
这里讨论的都是编程语言中的概念
若类A是B的子类,则记作A<=B,设有变换f(),若
1)当A<=B时,有f(A)<=B,则称变换f()具有协变性
比如f()是数组,A<=B,A[]<=B[],即存在B[] = A[] ,在Java中,实际上是成立的,那么就表示数组具有协变性。
2)当A<=B时,有f(B)<=f(A),则称f()具有逆变性
3)当A<=B时,f(A)与f(B)无关,则称f()具有不变性
泛型是不变的,在Java中,以下代码是不允许的
List<Super> super = new ArrayList<Sub>()
List<Sub> sub = new ArrayList<Super>()
所以说,泛型是不变的,因不变性带来使用上的不灵活,所以Java使用有界类型使得泛型可以支持协变与逆变。(这个我们在下面说明泛型的时候在解释。)
Java数组是协变的,当存在Teacher IS-A Person的情况,存在Teacher[] IS-A Person[],换句话说,如果需要的对象是Person [],那么我们是否可以传入Teacher []?答案是可以的。
比如说
public class Person{}
public class Teacher{}
则
Person[] arr = new Student[2];//是被允许的
这里的f()就是从类延伸到数组的变换,变换后原有的继承关系不变,所以说Java的数组是协变的。
而这里存在一些漏洞,比如:
arr[0] = new Teacher ();//编译期间会报警告,因为对arr来说,这是一个Student类型的数组,可实际arr[0]引用的是一个Teacher类型,但是Teacher IS-NOT Student,这样就产生了类型混乱,运行时系统并不能抛出ClassCastException异常,因为本身不存在类型转换,但是会抛出ArrayStoreException,因为Java中每个数组都声明了它所允许存储的对象的类型,如果将一个不兼容的类型插入数组,则会抛出该异常。
这是数组协变带来的静态类型漏洞,编译期间无法完全保证类型安全,看上去Java的设计者是在程序的易用性与类型安全之间做了取舍,如果不支持数组协变,一些通用的方法,如:Arrays.sort(Object[])确实无法正常工作。
5、Java伪泛型
在java5之前,java并不直接支持泛型实现,而是通过继承来的一些基本概念来实现泛型。
Q:Java5之前是如何具体实现泛型这一个概念呢?
A:使用Object表示泛型。
可以如下实现:
public class GenericType {
private Object value;
public void setValue(Object o) {
this.value = o;
}
public Object getValue() {
return value;
}
public static void main(String[] args) {
GenericType gt = new GenericType();
gt.setValue(new NestClass());
System.out.println(((NestClass) gt.getValue()).printer());
}
public static class NestClass {
public String printer() {
return "I'm printer";
}
}
}
但是使用这种策略时,有必要考虑,为了访问伪泛型类中的对象,调用该对象的方法,我们必须在使用时,将对象进行强转成对应的类型。
6、Java泛型特性
在Java5中,开始支持泛型,所以我们无需再主动对某些类型做类型转换。
对于一个泛型类的创建,在类的声明处包含一个或多个参数类型,这些参数被放在类名后的一对尖括号内。
public class GenericType<T,V> {}
但是泛型不支持基本数据类型,比如GenericType<int>这样是不被支持的。
一个泛型方法可以被以下方式定义
public static <T> T getValue(T value) {
return value;
}
但是需要注意的是,泛型类型T是不允许被直接实例化的,
比如T t = new T()这样是不被允许的。
对于泛型的不变性
存在一个接口Shape,内部定义了一个方法area,此时,定义一个泛型方法,传入的参数类型是Collection<Shape>。
假设现在有实现了Shape接口的子类Square,此时调用该泛型方法,传入Collection<Square>,但是由于泛型不是协变的,所以,我们不能把Collection<Square>作为参数传入进去。
如何对一个不变的泛型转换成支持协变和逆变?
使用通配符'?'+类型限界(extends\super关键字)
通配符用来表示参数类型的子类或超类
类型限界,即使用上方描述的方法,在尖括号内,使用extends、super关键字来指定参数类型必须具有的性质。
此时传入的参数变成了
Collection<? extends/super Shape>,此时,Collection<Square>就可以当做参数传入。
假设现有ABCDE五个类,继承关系为A<=B<=C<=D<=E,则<? extends C>代表元素可以是C或者是C的子类A,B;<? super C>代表元素可以是C或者是C的父类D、E。
由Collections.copy方法的原型看有界类型的应用
public static <T> void copy(List<? super T> dest, List<? extends T> src);
Collections.copy用作将src中的元素复制到dest的对应位置。方法执行后,dest对应的元素与src对应位置元素一致。使用extends与super,保证了src中取出的元素一定是dest元素的子类或相同类型。这样就不会在拷贝时产生类型安全问题。
可以通过另外一种写法也可以达到相同的效果。
public static <T, S extends T> T copy(List<T> dest, List<S> src);
对于有界类型,使用extends修饰的泛型容器不可写,同时,super修饰的泛型容器不可读(实际读出来的都是object类型。)
在使用extends有界类型时,所有以参数为类型的方法均不可用。当使用super有界类型时,所有以类型为返回值的方法均以Object替代返回值中的参数类型。
方法名是自解释的:T对应到参数类型作为方法的形式参数,V对应到参数类型作为方法的返回值。
泛型的类型擦除
Java中的泛型都是伪泛型,即在编译期间存在的泛型,在很大程度上是java语言中的成文而不是虚拟机中的结构。在编译期间,编译器会通过类型擦除,进而转换成非泛型类,这样,编译后就生成了一种与泛型类同名的原始类,但是类型参数都被删去了,类型变量由它们的类型限界来代替。而在外部使用泛型值时,编译器会自动插入类型转换代码。可以这么理解,如果在代码中定义List<String>,在编译后会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。
通过例子证明Java类型的类型擦除
1)原始类型相等
public class Test {
public static void main(String[] args) {
List<String> c1 = new ArrayList<>();
List<Integer> c2 = new ArrayList<>();
System.out.println(c1.getClass() == c2.getClass());
System.out.println(c1.getClass());
System.out.println(c2.getClass());
}
}
在上述例子中,我们定义了两个ArrayList数组,一个是ArrayList<String>,一个是ArrayList<Integer>,最后,我们通过c1对象和c2对象的getClass()方法获取它们的类信息,最后结果为true,说明泛型类型String和Integer都被擦掉了,只剩下原始类型。
2)通过反射添加其他元素类型
public class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> c1 = new ArrayList<>();
c1.add(1);
c1.getClass().getMethod("add", Object.class).invoke(c1, "222");
for (int i = 0; i < c1.size(); i++) {
System.out.println(c1.get(i));
}
}
}
在上述例子中,我们定义了一个List<Integer>泛型类型,如果直接调用add方法,那么我们只能存储整数数据,不过当我们利用反射调用add方法时,却可以存储字符串,这说明Integer泛型实例在编译后被擦除调了,只保留了原始类型。不过这里如果细心的朋友可能会发现,如果我们用c1.get(i)直接获取对应的类型,按理讲会出现类型转换异常,但实际没有,这个问题我们下面会讲到。
类型擦除后保留的原始类型
这里的原始类型表示的就是擦去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个类型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定类型则使用Object)替换。
3)原始类型Object
public static class Shape<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
Shape中的泛型被擦除后显示的原始类型为:
public static class Shape {
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
因为在Shape<T>中,T是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类。就像泛型还未出现之前,Java原本的实现方式。在程序中可以包含不同泛型类型的Shape,如Shape<Integer>,Shape<String>,但是擦除类型后,它们就成了原始的Object类型了。
如果类型变量有限定,那么原始类型就用第一个边界的类型变量替换
例如
public class Shape<T extends Comparable>{}
那么擦除后的原始类型就是
public class Shape<Comparable>{}
在调用泛型方法时,可以指定泛型,也可以不指定泛型。
1)在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级
2)在指定泛型的情况下,泛型变量的类型必须为该指定泛型类型
public static void main(String[] args) {
//不指定泛型
int i = getData(1, 2);//两个参数都是Integer,所以T为Integer
Number a = getData(1, 1.2f);//这两个一个是Integer,一个是Float,所以取同一父级的最小级,为Number
Object o = getData(1, "222");//去同级父类的最小级Object
//指定泛型类型
int x = Test5.<Integer>getData(1, 1);//指定泛型类型,则只能使用该泛型类型
}
public static <T> T getData(T data, T data2) {
T a = data2;
return a;
}
}
泛型类型擦除引起的问题及解决方法
1)既然说类型变量会在编译时擦除,那么如果我们往ArrayLis1t<String>创建的对象中添加整数,为何不能编译通过呢?
List<String> arr = new ArrayList<>();
arr.add("123");
arr.add(123);//报错
这是因为,Java编译器是通过先检查代码中的泛型类型,然后再进行类型擦除,再编译。而这个类型检查时针对的是定义对象时传入的泛型类型,所以,如果传入的是String,那么下面使用时,存储的数据就只能是String类型。
2)自动类型转换
因为类型擦除的问题,所有的泛型类型变量最后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?实际上,在编译的过程中,编译器已经帮我们做好了类型转换,所以不需要我们再进行手动转换。
3)泛型类型变量不能是基本数据类型
不能使用类型参数替换基本类型,因为类型擦除后,其原始类型Object或者其泛型界限不能存储基本数据类型。
4)泛型在静态方法和静态类
在泛型类中,static方法和static域均不可以引用类的类型变量,因为在类型擦除后类型变量就不存在了。实际的泛型类中的泛型参数是由实例化定义对象时指定的,另外,由于实际上只存在一个原始的类,因此,static域在该类的泛型实例之间是共享的。