Java学习打卡:第二十九天
内容导航
Java养成计划(打卡第29天)
学习内容:巩固最后一个关键模块:集合框架,调用数据结构
## 泛型巩固
分享前的介绍
或许这是最后的集中分析SE某个模块了,后面虽然还有GUI,但是只需要简单了解一下就行,主要时布局layout,和其中的一些组件的使用,因为后面会介绍一些前端的编程web,但是主要还是后端,用前端就可,后面SE收关项目虽然会用GUI,但其实其涉及不复杂。内容上,SE只需要查漏补缺了,毕竟这是一个持续学习的过程 。后面再多做几个项目练习,接下来简单介绍一下java8,就正式进入数据库,之后再分享Spring,SpringBoot
问题引入
在之前的Producer-Consumer多线程实例中,我们创建一个队列Queue,队列结点QueueNode有两个域,一个数据域data,一个指针域next(使用的链式结构),【虽然java没有指针的概念,但是这里还是以C的方式表达的,便于理解】,当时为了应对data的多样化,使用的是Object data;之后使用上转型传入Integer对象来操作
但是这种方式就要求我们传入对象时要明确其类型,比如Integer.valueOf(i) ,在后面出队时还有使用一次强制类型转换,非常麻烦,如果我们放入对象类型不对,编译器还不会报错,运行抛出ClassClastException例外-----泛型的引入就解决了这个问题,所以QueueNode可以定义为泛型类
泛型【巩固】
经过之前的了解,泛型可以在普通类型的基础上做进一步的抽象,包括有泛型类和泛型方法,使用<>来表示。提高代码的可读性和健壮性
几个名词
类型参数
: 尖括号中的元素,比如A,B,T
类型实参
: 使用泛型类时,给类型形参指定的一个具体类型
参数化类型
:包含类型实参的泛型类 ----- QueueNode< Integer>
使用之后,声明对象时也要加上<>,当类型不匹配就会及时报错,也不需要强制类型转换
约束类型参数(特殊泛型)
有的时候对于泛型中可以传入的类型时有要求的,就使用约束类型参数指定其超类,如果包含可选部分,则还必须实现其接口
public class QueueNode<T extends Number>{
····
}
这里类型实参只能时Number及其子类,不能时其他类型,比如String就时不合法的
泛型与子类型化
interesting上转型的问题^ - ^
由is-a的关系,就可以使用上转型变量调用子类中继承的重写的方法,实现多态
Integer num = Integer.valueOf(10); //ok
Number test = num; //上转型ok
这是普通的变量,那数组呢
Integer[] nums = new Integer[10];//创建数组
Number[] test = nums; //ok
其实nums和test都存储的是地址,就像C中的指针,这段代码是合法的,但是test虽然是Number型,但第二个指针调整指向之后,也就是指代的上面的Integer型数组,那么一旦赋予其他类型比如double型就会抛出例外ArrayStoreException
这是数组,那泛型呢?
QueueNode<Number> node = new QueueNode<Number>;
node.setData(Integer.valueOf(10)); //ok
node.setData("Hello Cfeng"); //Complie Error
这里因为传入了类型实参,所以就不能再传入非Number及其子类的类型
⚠ G< Sub >不是G< Super >的子类!
再来看下面一段代码
QueueNode<Integer> node1 = new QueueNode<Integer>;
QueueNode<Number> node = node1; //complie error
node.setData(Integer.valueOf(10));
为什么会编译错误,这是因为QueueNode< Integer>不是QueueNode< Number> 的子类,这里一旦装载了类型实参就是不可变的,Integer就只能传Integer型对象。普遍上说,G< Sub >不是G< Super >的子类!
通配符
考虑再泛型QueueNode< T>的定义中添加一个静态的print方法,该方法接受任意一个类型的队结点并将对象取出输出,难道使用Object?
public class QueueNode<Object>{
····
}
public static void print(QueueNode<Object> b)
{
Object o = b.getData();
System.out.println(o);
}
这个方法的用途是非常受限的。因为QueueNode< Integer>不是QueueNode< Object>的子类型,所以不能接收这些类型,如何下能接收任意类型呢?
这个时候就要使用通配符了,就是里面不传入Object而是?
public static void print(QueueNode<?> b)//这里就可以传入任意类型的队结点
{
Object o = b.getData();
System.out.println(o);
}
我们使用的QueueNode< ?>是一种未知类型,如果进行强制类型转换,编译器会给出一个未经检查的警告⚠
约束通配符
在继续分享之前说一个报错,就是eclipse中无法构建文件
eclipse.buildId=4.20.0.I20210611-1600 java.version=16.0.2 java.vendor=Oracle Corporation BootLoader constants: OS=win32, ARCH=x86_64, WS=win32, NL=zh_CN Framework arguments: -product org.eclipse.epp.
这个报错出现的时候,无法新建项目,并且之前的项目无法打开,这确实让我有点焦头烂额,后面才发现这是由于eclipse之前非正常关闭或者进程拥堵
- 解决的办法就是 找到workspace目录下的.metadata.plugins\org.eclipse.core.resources目录,删除文件
- 或者简单一点: 断网 重开
通配符的作用就是解决上面的方法中调用泛型类中的问题,泛型类一旦传入实参类型就固定了,不存在继承的问题,但是这样子又扩大范围了,现在里面可以传入任意类型的数据,所以需要限制,约束,形成约束通配符
QueueNode<? extends Number>
QueueNode<? super Integer> //指定子类的超类
泛型方法
类可以泛型,方法自然可以泛型,泛型方法得到普遍使用
语法和泛型类类似,也是<>
类型参数使用限制
泛型的类型安全由编译器承担
编译器是如何处理泛型的呢?
类型擦除技术
编译泛型后将擦除所有的泛型信息,泛型类类体中出现的类型参数由其上限类型取代,通常是Object,参数化类型如QueueNode< T>就转化为QueueNode,类型不正确时就进行强制类型转换【这就是不使用泛型的操作】
所以java运行系统从来就不涉及泛型类的处理,都是当作普通类处理,一个泛型类编译只会产生一个.class文件,所有该泛型的参数化类型共享该文件
基于泛型的擦除技术,所以泛型的使用会受到一定的限制,对有些泛型代码,如果编译器不能保证正确性,就会给出警告或错误
- 静态变量和方法为所有实例共享,所以在
静态环境中不能使用泛型
public Box<T>{
pulic static void m1(T x){//编译错误,该方法为static,不能使用泛型
···
}
}
- 不能创建为指定类型参数的对象(编译器不知道类型,不能创建)
public void m2(Object o)
{
T obj = new T(); //编译错误,不能创建未知类型的对象
}
- 不能创建为指定的类型参数或者参数化类型的数组对象(除非时无约束通配符)
public void me(Object o)
{
T[] num = new T[10];//编译错误,不知道类型
}
- 不能判断一个实例对象是否为参数化类型或者类型参数的对象(除非类型实参时无约束通配符
public void m2(Object o)
{
boolean f = o instanceof T; //T是未知的,不能判断o是其对象
}
- 不能用类型参数作为强制化类型转换的目标对象
- 不能将原始类型强制转化为参数化类型
T item = (T)o //不能进行类型转换
Box<Integer>[] nums = new Box<Integer>[10]; //编译错误
boolean f = o instanceof Box<Integer>; //编译错误
Box<Integer> box = (Box<Integer>)o; //编译警告
集合巩固
集合框架概述
集合通常是存储对象的容器,这些对象称为集合的元素。Java集合框架提出了一个用于表示和操作集合的体系结构、提供了一组相应的结构和类。应用程序员可以实现或扩展其中的某个接口或类来定义专门的集合类,以完成较为特殊的集合的表示和操作任务;也可以直接使用框架中现有的实现类,完成较为一般的集合的表示和操作任务。
Java集合框架主要支持3种类型的集合:表、集、映射。
-
表----存储一组顺序排列的元素;
-
集----存储一组互相不同的元素;
-
映射-----存储一组键值对;
Collection(根接口)
Collection接口是所有根和集的根接口
,定义了所有表和集的共同的行为方法。
public interface Collection<E> extends Iterable<E>{
boolean add(E,e); //为当前集合添加一个元素(是否添加成功) 这是一个泛型
boolean addAll(Collection<?extends E> c); //将集合c的所有元素添加到当前集合
boolean remove(Object o); //从当前集合中删除o
boolean removeAll(Collection<?> c) //从当前集合中删除集合c的所有元素
boolean retainAll(Collection<?> c) //从当前集合中删不属于集合c的所有元素
void clear(); //删除当前集合中的所有元素
boolean isEmpty(); //删除当前集合是否为空
int size(); //当前集合中元素的个数
boolean equals(Object o); //判断集合是否相等
int hashCode(); //获得当前集合的散列码
Iterator<E> iterator(); //返回当前集合的一个迭代器
······
}
注意每一个集合Collection都由迭代器,Map没有,因为向量没有迭代器,这里使用了装饰器模式,还有一些方法诸如toArray, toString之类的就不一一介绍了
总的分析一下
add和remove方法完成单个元素的添加和删除,addAll、removeAll、retainAll方法类似集合的并、差、∩运算。返回值都是boolean,如果成功改变集合就返回true
两个toArray方法集合中的元素装入指定的数组当中
Collection<String> c = ····
String[] ss = c.toArray(new String[0]);//ss指向放入数组的地址
这里如果数组装不下,就会创建一个相应大小的数组,如果不是同类型或者超类型,就会抛出例外ArrayStoreException
AbstractCollection
是一个实现Collection的抽象类,因为我们使用时只会用到少量的方法,所以这就是给了一个适配器,除了size方法和itreator方法,其余方法都给了实现
但是add方法只是抛出了例外,equals方法和hashCode方法都只是简单从Object类继承而来,没有进一步实现
该抽象类对Object的toString方法进行了覆盖,可以返回集合的字符串表示,返回字符产的格式为:包含在方括号内,由逗号间隔,各元素通过String.valueOf()转化为字符串,各元素顺序取决于迭代器返回元素的顺序
Iterator
这是一个Collection对象关联的属性,iterator方法可以返回当前集合的一个迭代器对象,java.util包内的Iterator接口就定义了该类对象的行为和方法。这是一个泛型接口
public interface Iterator<E>{
boolean hasnext(); //是否还有元素没有访问
E next(); //返回下一个元素
void remove(); //从集合中删除刚刚由next方法返回的元素
}
迭代器内部有一个游标,总位于两元素之间,next方法实际上返回的是游标后面的元素,并使游标后移动一个位置
这里就还是简单看一下基本操作
package Luogu;
import java.util.*;
public class ContainerDemo {
public static void main(String[] args) {
List<String> s = new ArrayList<String>(); //这里使用上转型,可以传入其他类型的表
s.add("hello");
s.add("cFeng");
s.add("study");
System.out.println(s.toString());//打印出数组,这是继承的的AbstractCollection的方法
System.out.println(s.size());
System.out.println(s); //这种方法也可以,不要觉得使地址哦
s.set(2, "how are you"); //修改
System.out.println(s);
//使用迭代器for-each遍历
Iterator<String> iterator1 = s.iterator();//对象通过表的方法获取
while(iterator1.hasNext())
{
System.out.print(iterator1.next()+" ");
}
for(int i = 0;i < s.size();i++)
{
System.out.print(s.get(i)+" ");
}
//利用数组表创建链式表【顺序表,链表】
LinkedList<String> ls = new LinkedList<>(s);
ls.add(0, "你好");
ls.add(3,"我是Echo"); //链表就不会存在越界的问题,可以方便在任何位置插入,而顺序表就不具备这个功能
System.out.println(ls);
ListIterator<String> iterator2 = ls.listIterator(ls.size());
while(iterator2.hasPrevious())
{//链式表才有从后向前的功能
System.out.println(iterator2.previous());
}
}
}
输出的结果为
[hello, cFeng, study]
3
[hello, cFeng, study]
[hello, cFeng, how are you]
hello cFeng how are you hello cFeng how are you [你好, hello, cFeng, 我是Echo, how are you]
how are you
我是Echo
cFeng
hello
你好
这里涉及到的链表,数组表,以及栈,队列,向量,树,还有hash表结构就是数据结构的内容了,明天简单看一下,以后会推出相关专栏,各位一起加油~