目录
前言
本章节是在IntelliJ IDEA集成开发环境测试的结果,内容多适用于学习Java方向数据结构的学者
一、复杂度
Ⅰ、时间复杂度与空间复杂度
时间复杂度:算法中基本代码的运行次数
空间复杂度:为实现一个算法而创建的空间次数
例如:
void func(int n){
int count = 0;
//时间复杂度为n
for(int i = 0 ; i < n ; i++){
count++;
}
//时间复杂度为n^2
for(int i = 0; i < 2 * n;i++){
count++;
}
int M = 10;
//时间复杂度为10
while(M > 0){
M--;
count++;
}
}
Ⅱ、大O的渐进表示法
原理:复杂度相加,保留最高次项数,舍去常数项。
例如:由上代码复杂度如下
func( n ) = n^2 + 2*n + 10
保留最高次项数,舍去常数项,就是这个方法的时间复杂度。记作O(n^2)
空间复杂度同理
原因:
n = 10时: func = 130
n = 100时: func = 10210
n = 10000时: func = 100020010
n = 1000000时: func = 1000002000010
可以发现当 n 越大时 ,结果越接近最高次项数(n^2)。
二、泛型
Ⅰ、为什么会有泛型
我们在学完java SE之后只能使用具体的类型(要么是基本类型 int char等等,要么是自定义类),如果需要编写可以应用于多种类型的代码,那么之前学的语法就会有很大的束缚。所以在JDK1.5之后引入了新的泛型语法,通俗讲,泛型:就是适用于多种类型,你想存什么类型数据或者自定义类都能实现。
由以下实列证明束缚在哪:
问题:
①Object数组任何类型数据都可以存放,但是具体存放的数据类型我们并不知道,后续需要调用时就显得很麻烦,所以直接用Object数组来存放数据不安全。
②1 号下标本身就是字符串,但是编译却报错了。必须进行强制类型转换。
首先我们知道Object为所有基本类型的父类,那么我们可以创建一个Object数组来存放各种基本类型,但是当需要使用这些数据时,我们不知道object中存放的是那种数据类型,即使知道也需要进行强制转换,这显得有点不太灵活,简而言之就是被束缚了。
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能持有一种类型。而不是同时持有这么多类型。
所以泛型的主要目的:指定当前容器要持有什么类型的对象,让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
Ⅱ、泛型语法
class 类型名称<指定类型形参列表>
例:class Main<Integer> 、 class MyClass<Integer,Character>、class Text<Main<Integer>>
由以上代码改编如下
class MyArray<T> {
public T[] array = (T[])new Object[10];//①
//获得指定位置的元素
public T getPos(int pos) {
return this.array[pos];
}
//设置指定位置的元素
public void setVal(int pos,T val) {
this.array[pos] = val;
}
}
public class Main {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray();//②
myArray.setVal(0,10);
myArray.setVal(1,12);
int ret = myArray.getPos(1);
System.out.println(ret);
myArray.setVal(2,"bit");//③编译报错
}
}
问题:
①在代码①处为什么不这样写 public T[] array = new T[10];
解释:java中不能new一个泛型数组,知识点:为什么不能创建泛型数组
② 泛型的尖括号当中只能使用引用类型(Integer),不能用简单类型(int)
③编译报错,编译器会自动帮我们检测存入的类型是否与指定类型相同,不相同就会报错,相比不使用泛型语法,这样更不容易出错。
④以上我们强制转换了Object数组,这种操作并不是很好,因为这样做你只是骗过了编译器,常规操作如下创建泛型数组常规操作
由此,以上MyArray中可以存放我们指定的数据类型
课外知识:
【规范】类型形参一般使用一个大写字母表示,常用的名称有:
E 表示 Element K 表示 Key
V 表示 Value N 表示 Number T 表示 Type S、U、V等---- 第二、第三、第四个类型
Ⅲ、裸类型(了解即可)
直接上例子:
class MyArray<T> {
public T[] array = (T[])new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,T val) {
this.array[pos] = val;
}
}
public class Main {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0,12);
myArray.setVal(1,"hello");
}
}
以上编译不报错,运行也没问题。
可以看到我们没使用尖括号指定泛型类,这样的类型就叫做裸类型。但是我们不要自己去使用裸类型,裸类型只是为了兼容老版本的API而保留的机制。以下擦除机制,会提到编译器是如何使用泛型的。
Ⅳ、泛型如何编译?
①、擦除机制
查看字节码文件方式:
①通过命令javap -c
②通过idea附加插件(jclasslib,需要手动下载)
图一中array原本是T[]类型的,但是编译之后变成了Object类型。
图二中setVal方法中形参val原本是T类型,编译之后却变成了Object类型。
观察所有T出现的地方,我们都发现编译之后T类型都变成了Object类型,那么我们就把这种编译机制叫做擦除机制,由此可以说明在编译后,编译器生成的字节码文件不存在泛型这个概念。
注:字节码文件中
更详细的泛型擦除机制介绍————<泛型擦除机制>
知识点:为什么不能创建泛型数组
由以上擦除机制我们知道如果创建了一个泛型数组,那么在编译之后就变成了Object数组,而你就可以存放任意类型数据,那么你就可以乱来,随便存放数据,所以编译器为了规范这种操作,就不让我们创建泛型数组。从另一方面来讲你只能用Object[]来接收Object数组,因为Object是所有类的父类,而你并不知道里面存放的数据类型,需要取值的时候很不安全。
创建泛型数组常规操作:
我们原本时这样创建泛型数组的:
public T[] array = (T[])new Object[10];
由擦除机制我们知道array在编译之后变成了Object类,而如果你想在外部得到这个数组,那么就有以下问题:
public class MyArray<T> {
public T[] array = (T[])new Object[10];
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<Integer> myArray1 = new MyArray<>();
Integer[] strings = myArray1.getArray();
}
}
以上代码编译器并不会报错,看着逻辑确实没问题,但是运行时出现了异常:
意思时Object类型不能由Integer类型接收
也就是说你自己创建了泛型数组其实就是Object数组,这样还是没解决不安全的问题。那么我们还可以通过反射,创建指定类型的数组。
import java.lang.reflect.Array;
class MyArray<T> {
public T[] array;
public MyArray() {
}
//构造时指定类型和容量
public MyArray(Class<T> 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<Integer> myArray1 = new MyArray<>(Integer.class, 10);
Integer[] integers = myArray1.getArray();
}
}
Ⅴ、泛型的边界
我们在定义泛型时,如果不指定泛型的上界,那么默认泛型的上界为Object,如
public class MyArray<T>
public class MyArray<T extends Object>
这里extends不是继承的意思,表示的是T的上界为Object。
这两种定义方式都相同。那么为什么有这种机制呢?
在编译之后编译器会将泛型擦除成泛型的上界,比如T擦除成Object。这样你需要指定类型时只能指定上界(Object)及上界的子类,指定其他的类会报错。下面举个例子来快速理解这样做的好处。
例:如果我们想指定一种类型数组,找到这种类型数组的最大值 ,应该如何求解呢
或许我们第一时间会想到遍历数组用等于号比大小,但是这是错误的,我们指定的类型是引用类型,不是基本类型,本身不能比较大小,这时候就可以使用泛型上界擦除机制:
public class MyArray<T extends Comparable<T>> {
public T finMax(T[] array){
T max = array[0];
for (int i = 1; i < array.length; i++) {
if(array[i].compareTo(max) > 0){
max = array[i];
}
}
return max;
}
}
比较大小的时候max与array被擦除成了Comparable类型,而Comparable中有比较方法,当然T需要实现Comparable接口
Ⅵ、泛型方法
顾名思义:含有泛型的方法,但是这个方法可以是静态方法。因为静态静态方法不依赖于对象,所以可以使用泛型方法实现。
格式:方法限定符 返回值类型 方法名称(形参列表)
例如以上代码可以改编成:
class Text {
public static<T extends Comparable<T>> T finMax(T[] array){
T max = array[0];
for (int i = 1; i < array.length; i++) {
if(array[i].compareTo(max) > 0){
max = array[i];
}
}
return max;
}
}
public class MyArray {
public static void main(String[] args) {
Integer[] arr = {1,34,123,4235,546,2};
Integer max = Text.finMax(arr);
System.out.println(max);
}
}
Ⅶ、通配符(?)
①、通配符解决什么问题
通配符是用来解决泛型无法协变的问题,协变指的是如果Water是Object的子类,那么List<Water>也应该是List<Object>的子类。但是泛型不支持这样的父子类关系。
下面是一个通配符的例子(代码有错误):
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TextDemo {
public static void main(String[] args) {
Message<String> message = new Message() ;
Message<Integer> message1 = new Message<>();
message.setMessage("hello");
message1.setMessage(111);
fun1(message);
fun1(message1);
fun2(message);
fun2(message1);
}
public static void fun1(Message<String> temp){
System.out.println(temp.getMessage());
}
public static void fun2(Message<?> temp){
System.out.println(temp.getMessage());
}
}
可以看到fun1指定了只能传入String类型的泛型,而fun2中使用了通配符可以传通配符范围内任意的数据类型。
②、 通配符的上界
格式:public static void fun1(Message<?extends Water> temp)
表示在fun1方法传参时,泛型的类型只能是Water及Water的子类。
③、通配符的下界
格式:public static void fun1(Message<?super Water> temp)
表示在fun1方法传参时,泛型的类型只能是Water及Water的父类
④、包装类
在java中 ,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
装箱与拆箱:
装箱:将基本数据类型变为包装类类型
Integer a = 1;//自动装箱、隐式装箱
Character b = new Character('f');//手动装箱、显式装箱
拆箱:将包装类类型变为基本数据类型
Integer integer = 10;//自动装箱
int val = integer;//自动拆箱
int val1 = integer.intValue();//手动拆箱