在学习完JavaSE之后,开始进入数据结构的学习,Java当中已经有实现好的一些集合类(或者说是一些已经由Java实现好的数据结构)。本文主要是一些预备知识,包括初始基础框架(有个印象即可)、时间复杂度和空间复杂度、泛型的知识。
在进入学习之前,得先了解什么是数据结构。
1、数据结构是一门单独的学科,不存在Java的数据结构和C语言的数据结构不一样的情况,两个语言的数据结构主要区别只有一个,就是实现的语言不同而已。
2、数据结构 和 数据库 是两个不同的东西,数据结构算是一种概念化的、比较抽象的东西,数据库是用来存储数据的,而数据库在存数据的时候,底层回用到数据结构。
3、数据结构其含义实际上是 数据 + 结构,-> 用来描述和组织一组数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
4、不同的数据结构的应用场景不同,因此产生了各种各样的数据结构。
目录
一、什么是集合框架
Java集合框架 Java Collection Framework,又被称为容器 container,是定义在 java.util 包下的一组接口 interfaces 和其实现类 classes。(下图为一些比较重要的接口和类)
其主要表现为将多个元素 element 置于一个单元中,用于对这些元素进行快速、便捷的存储 store、检索 retrieve、管理 manipulate,即我们俗称的增删改查 CRUD。
例如,一个通讯录(一组电话和姓名的映射关系)、一副扑克牌(一组牌的集合)等。


上图是我们主要学习的容器,每个容器其实都是对某种特定数据结构的封装。
1. Collection:是一个接口,包含了大部分容器常用的一些方法
2. List:是一个接口,规范了ArrayList 和 LinkedList中要实现的方法
ArrayList:实现了List接口,底层为动态类型顺序表
LinkedList:实现了List接口,底层为双向链表
3. Stack:底层是栈,栈是一种特殊的顺序表
4. Queue:底层是队列,队列是一种特殊的顺序表
5. Deque:是一个接口
6. Set:集合,是一个接口,里面放置的是K模型
HashSet:底层为哈希桶,查询的时间复杂度为O(1)
TreeSet:底层为红黑树,查询的时间复杂度为O(log₂N),关于key有序的
7. Map:映射,里面存储的是K-V模型的键值对
HashMap:底层为哈希桶,查询时间复杂度为O(1)
TreeMap:底层为红黑树,查询的时间复杂度为O(log₂N),关于key有序
二、算法效率
(此处只讲概念,示例将单独写一篇博客)
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。
时间效率被称为时间复杂度,而空间效率被称作 空间复杂度。
时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。平时所说的时间/空间复杂度都是最坏情况下的结果。
2.1 时间复杂度
算法的时间复杂度是一个数学函数,定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。用大O渐进表达式法。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
常见的渐进时间复杂度为:
O(1) < O(log₂n) < O(n) < O(nlog₂n) < O(n²) < O(n³) < O(2ⁿ) < O(n!) < O(nⁿ)
2.2 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。算的是变量的个数,其计算规则与时间复杂度类似,也使用大O渐进表达法。
例如创建了一个新数组就是开辟了新内存,其空间复杂度就是数组的长度。同时需要注意,递归一定会开辟内存。
若算法的空间复杂度为 O(1),表示该算法所需辅助空间大小与问题规模 n 无关。
三、包装类
在Java中,由于基本类型不是继承 Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都封装成对象的形式,也就是包装类型。(int 和 char 的包装类较特殊,其他的都是首字母大写)
| 基本数据类型 | 包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
四、装箱和拆箱
4.1 概念
| 名称 | 含义 | |
|---|---|---|
| 装箱 / 装包 | 自动装箱 显示装箱 | 把 基本数据类型 变为 包装类类型 的过程 |
| 拆箱 / 拆包 | 自动拆箱 显示拆箱 | 把 包装类类型 变为 基本数据类型 的过程 |
int i = 10;
// 自动装箱
Integer i2 = Integer.valueOf(i); // 显示装箱
Integer i3 = 10;
//Integer i3 = (Integer)i;
// 自动拆箱
int i4 = Integer.intValue(i2); // 显示拆箱
int i5 = i4;
对于语句 Interger i3 = 10; 底层帮我们调用了 Integer.valueOf() 方法(可在cmd通过反汇编命令javap -c查看);语句 int i5 = i4; 同理,底层帮我们调用了 Integer.intValue()。
4.2 面试题
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println(a == b);
Integer c = 128;
Integer d = 128;
System.out.println(c == d);
}
上面的程序将会输出以下结果:
true
false
因为上面的4个变量都属于装箱。要想知道为什么会得到这样的结果,就需要查看装箱是怎么操作的。Alt+鼠标左键进入 Integer 类所在文件。
得到如下信息:
public static Integer valueOF(int i){
if(i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(I);
}
该代码说明如果传入的正数 i 在一个范围内,那么返回的是数组中的值;不在这个范围的时候,是返回新的对象。用 == 比较两个新的对象,肯定不一样。
而这个范围是 [-128, 127] ,总共256个数字,下标是 0~255。
五、泛型
泛型,通俗来讲就是可应用于多种类型的代码;从代码上讲,就是对类型实现了参数化(根据传入参数类型决定该 泛型类 / 泛型方法 的类型)。
5.1 引出泛型
问:可否实现一个类,类中包括一个数组的成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
思路:因为所有的类都继承于 Object 类,试着将数组创建为 Object 类型。
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos,Object val) {
this.array[pos] = val;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0,10);
myArray.setVal(1,"hello");//字符串也可以存放
//String ret = myArray.getPos(1);//编译报错
String ret = (String)myArray.getPos(1);
System.out.println(ret);
}
}
由上面的代码我们看出存在两个问题:
1、存放的数据没统一,很杂乱;
2、每次取数据的时候,如1号下标本身就是字符串,但是必须要强制类型转换才能运行。
-> 能否让编译器自动检查类型;能否让编译器自动类型转换?
此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
5.2 语法
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
类后面的 <T> 代表占位符,表示当前类是一个泛型类,用于接收传入的参数。
【规范】类型形参一般使用一个大写字母表示,常用的名称有:
E 表示 ElementK 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型
示例:
package test_8_4;
class MyArray<E>{
public Object[] array = new Object[10];
public void setValue(int pose, E val){
array[pose] = val;
}
public E getValue(int pose){
return (E)array[pose];
}
}
public class test {
public static void main(String[] args) {
MyArray list = new MyArray(); // 裸类型
MyArray<Integer> array = new MyArray<Integer>();
// 实例化第2种方法,后面的<>不同写包装类
//MyArray<Integer> array = new MyArray<>();
// 错误写法:不能直接写数据类型
//MyArray<int> array = new MyArray<int>();
System.out.println(array);
System.out.println("--------------------");
MyArray<String> array1 = new MyArray<String>();
System.out.println(array1);
MyArray2.set(2,"hello"); // 不需要强制类型转化
}
}
(实例化的第2种方法,即省略了后面的类型,因为编译器可以根据上下文进行类型推导,因此可以省略类型实参的填写。)
上面的代码可以实现自动类型检查:

也可以实现自动类的转化:

程序运行输出结果:
test_8_4.MyArray@4554617c
--------------------
test_8_4.MyArray@74a14482
我们发现两种不同类型(一个是MyArray<Integer>,另一个是MyArray<String>)在运行之后回到了同一个类型(MyArray)。
原因是因为泛型是编译时的一种机制,在运行的时候并没有泛型的概念。而是在编译的过程中,将所有 <> 里面的类型替换成 Object ,这种机制被称为 擦除机制。
5.3 泛型的上界
语法形式如下:
class 泛型类名称<类型形参 extends 类型边界> {
...
}
示例:

上面的 extends Number 指定了传入参数 E 的一个界限,规定传入的参数只能是 Number 或者是 Number 的子类。
5.4 复杂示例
引子:编写一个泛型类,其中有一个方法,可以找到数组的最大值。

上图中报错的原因在于,Java在编译的时候将泛型类擦除成 Object 类,而 Object 属于引用类型,是无法直接使用比较符号对两个值进行比较的。
那么,对于引用类型,比较的方式是实现 Comparable 接口,使其具有可比性。然后就可以调用该接口中的 compareTo 方法对两个变量进行比较:
class Alg<E extends Comparable<E>>{
public E findMax(E[] nums){
E max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (max.compareTo(nums[i]) < 0){
max = nums[i];
}
}
return max;
}
}
public class TestGeneric {
public static void main(String[] args) {
Integer[] array = {2,14,5,69,43,22,6,0};
Alg<Integer> alg = new Alg<>();
int r = alg.findMax(array);
System.out.println(r);
}
}
再如包装类不是 Interger,而是自定义类型呢?

该类同样需要实现 Comparable 接口才能被使用:
class Alg<E extends Comparable<E>>{
public E findMax(E[] nums){
E max = nums[0];
for (int i = 0; i < nums.length; i++) {
/*if (max < nums[i]){
max = nums[i];
}*/
if (max.compareTo(nums[i]) < 0){
max = nums[i];
}
}
return max;
}
}
class Person implements Comparable<Person>{
@Override
public int compareTo(Person o) {
return 0;
}
}
public class TestGeneric {
public static void main(String[] args) {
Alg<Person> alg1 = new Alg<Person>();
}
}
5.5 泛型方法
我们不将类定义成泛型类了,而是将方法定义成泛型方法。下面是具体做法:
class Alg2{
public<E extends Comparable<E>> E findMax(E[] nums){
E max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (max.compareTo(nums[i]) < 0){
max = nums[i];
}
}
return max;
}
}
public class TestGeneric {
public static void main(String[] args) {
Alg2 alg2 = new Alg2();
Integer[] array = {3,5,1,36,97,56};
int r = alg2.findMax(array);
//int r = alg2.<Integer>findMax(array); // 两种写法
System.out.println(r);
}
}
是将 <E extends Comparable<E>> 语句放在方法的返回类型前,使得整个方法都可以识别到 E。
静态的泛型方法如何使用:直接用类名点出方法
class Alg3{
public static <E extends Comparable<E>> E findMax(E[] nums){
E max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (max.compareTo(nums[i]) < 0){
max = nums[i];
}
}
return max;
}
}
public class TestGeneric {
public static void main(String[] args) {
Integer[] array = {3,5,1,36,97,56};
int r = Alg3.findMax(array);
System.out.println(r);
}
}
5456

被折叠的 条评论
为什么被折叠?



