数据结构与算法(系列文章一)

本系列是关于数据结构与算法内容,系列内容出自“数据结构与算法分析-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 = new ArrayList()
        List   sub = new ArrayList()
        所以说,泛型是不变的,因不变性带来使用上的不灵活,所以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 {}

但是泛型不支持基本数据类型,比如GenericType这样是不被支持的。

一个泛型方法可以被以下方式定义

 public static  T getValue(T value) {
      return value;
}

但是需要注意的是,泛型类型T是不允许被直接实例化的,

比如T t = new T()这样是不被允许的。

对于泛型的不变性

存在一个接口Shape,内部定义了一个方法area,此时,定义一个泛型方法,传入的参数类型是Collection

假设现在有实现了Shape接口的子类Square,此时调用该泛型方法,传入Collection,但是由于泛型不是协变的,所以,我们不能把Collection作为参数传入进去。

如何对一个不变的泛型转换成支持协变和逆变?

使用通配符'?'+类型限界(extends\super关键字)

通配符用来表示参数类型的子类或超类

类型限界,即使用上方描述的方法,在尖括号内,使用extends、super关键字来指定参数类型必须具有的性质。

此时传入的参数变成了

Collection,此时,Collection就可以当做参数传入。

假设现有ABCDE五个类,继承关系为A<=B<=C<=D<=E,则代表元素可以是C或者是C的子类A,B;代表元素可以是C或者是C的父类D、E。

由Collections.copy方法的原型看有界类型的应用

public static  void copy(List dest, List src);

Collections.copy用作将src中的元素复制到dest的对应位置。方法执行后,dest对应的元素与src对应位置元素一致。使用extends与super,保证了src中取出的元素一定是dest元素的子类或相同类型。这样就不会在拷贝时产生类型安全问题。

可以通过另外一种写法也可以达到相同的效果。

public static  T copy(List dest, List src);

对于有界类型,使用extends修饰的泛型容器不可写,同时,super修饰的泛型容器不可读(实际读出来的都是object类型。)

在使用extends有界类型时,所有以参数为类型的方法均不可用。当使用super有界类型时,所有以类型为返回值的方法均以Object替代返回值中的参数类型。

方法名是自解释的:T对应到参数类型作为方法的形式参数,V对应到参数类型作为方法的返回值。

泛型的类型擦除

Java中的泛型都是伪泛型,即在编译期间存在的泛型,在很大程度上是java语言中的成文而不是虚拟机中的结构。在编译期间,编译器会通过类型擦除,进而转换成非泛型类,这样,编译后就生成了一种与泛型类同名的原始类,但是类型参数都被删去了,类型变量由它们的类型限界来代替。而在外部使用泛型值时,编译器会自动插入类型转换代码。可以这么理解,如果在代码中定义List,在编译后会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。

通过例子证明Java类型的类型擦除

1)原始类型相等

public class Test {

    public static void main(String[] args) {
        List c1 = new ArrayList<>();
        List c2 = new ArrayList<>();
        System.out.println(c1.getClass() == c2.getClass());
        System.out.println(c1.getClass());
        System.out.println(c2.getClass());
    }
}

在上述例子中,我们定义了两个ArrayList数组,一个是ArrayList,一个是ArrayList,最后,我们通过c1对象和c2对象的getClass()方法获取它们的类信息,最后结果为true,说明泛型类型String和Integer都被擦掉了,只剩下原始类型。

2)通过反射添加其他元素类型

public class Test {

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List 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泛型类型,如果直接调用add方法,那么我们只能存储整数数据,不过当我们利用反射调用add方法时,却可以存储字符串,这说明Integer泛型实例在编译后被擦除调了,只保留了原始类型。不过这里如果细心的朋友可能会发现,如果我们用c1.get(i)直接获取对应的类型,按理讲会出现类型转换异常,但实际没有,这个问题我们下面会讲到。

类型擦除后保留的原始类型

这里的原始类型表示的就是擦去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个类型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定类型则使用Object)替换。

3)原始类型Object

public static class Shape {
        
        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是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类。就像泛型还未出现之前,Java原本的实现方式。在程序中可以包含不同泛型类型的Shape,如Shape,Shape,但是擦除类型后,它们就成了原始的Object类型了。

如果类型变量有限定,那么原始类型就用第一个边界的类型变量替换

例如

public class Shape{}

那么擦除后的原始类型就是

public class Shape{}

在调用泛型方法时,可以指定泛型,也可以不指定泛型。

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.getData(1, 1);//指定泛型类型,则只能使用该泛型类型
    }


    public static  T getData(T data, T data2) {
        T a = data2;
        return a;
    }
}

泛型类型擦除引起的问题及解决方法

1)既然说类型变量会在编译时擦除,那么如果我们往ArrayLis1t创建的对象中添加整数,为何不能编译通过呢?

List arr = new ArrayList<>();
arr.add("123");
arr.add(123);//报错

这是因为,Java编译器是通过先检查代码中的泛型类型,然后再进行类型擦除,再编译。而这个类型检查时针对的是定义对象时传入的泛型类型,所以,如果传入的是String,那么下面使用时,存储的数据就只能是String类型。

2)自动类型转换

因为类型擦除的问题,所有的泛型类型变量最后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?实际上,在编译的过程中,编译器已经帮我们做好了类型转换,所以不需要我们再进行手动转换。

3)泛型类型变量不能是基本数据类型

不能使用类型参数替换基本类型,因为类型擦除后,其原始类型Object或者其泛型界限不能存储基本数据类型。

4)泛型在静态方法和静态类

在泛型类中,static方法和static域均不可以引用类的类型变量,因为在类型擦除后类型变量就不存在了。实际的泛型类中的泛型参数是由实例化定义对象时指定的,另外,由于实际上只存在一个原始的类,因此,static域在该类的泛型实例之间是共享的。

你可能感兴趣的:(Java)