泛型
本质上是指类型参数化
泛型的继承
泛型类是可以继承的,任何一个泛型类可以作为父类或者子类,不过和普通的继承略有不同
父类 public class A{}
-
泛型的子类必须将将泛型父类所需要的类型参数沿着继承链向上传递
- 子类不是泛型类则需要给泛型父类传递类型常量
public class B extends A<String>{}
- 子类时泛型类可以给父类传递类型常量,也可以传递类型变量
public class B<T> extends A<T>{}
-
如果父类的泛型中有约束
public class A<T extends Serializable>{}
public class B<D extends Serializable> extends A<D>{}
特殊写法:
public class B<D extends Serializable> extends A{}
public class B3<ID> extends A{}
和实现泛型接口的规则一致
泛型的擦除[面试]
实际上从VM的角度来说是不存在泛型的概念,泛型是运用在编译期的技术,在编译时编译器会按照泛型声明对容器内的元素进行检查,检查不匹配则编译失败;如果全部检查成功则编译通过。但是编译通过后生成的.class字节码文件中并没有泛型声明的标识,也就是文件中并不会出现泛型,这就是泛型擦除。
- 在.java文件中运行泛型技术时,编译器在文件编译通过后自动擦除泛型标识
由于泛型的擦除,类文件中实际上并没有泛型机制,同时也没有使用向下类型转换,为什么运行时,没有出现异常?
因为泛型的补偿。编译器在擦除泛型后,会自动将类型转换为原定义的泛型,这样就不必在做窄化操作。
泛型的擦除和补偿都是编译器内部完成的。
泛型的局限性
- 不能使用基本数据类型,可使用对应类型的包装类,例如Integer
- 不是使用泛型类异常
- 不能使用泛型数组,声明泛型数组可以,但不可以实例化
- 不能实例化参数类型对象
T obj = new T();(错误)
如果需要实例化则需要通过反射机制
对象的克隆
Object 类中定义clone方法可以实现对象的浅克隆,使用克隆的方式创建对象的成本远远小于new操作。【原型模式】
如果一个需要支持浅克隆操作,则需要实现Cloneable接口【标志接口】,用于告知VM这个类型的对象需要支持克隆操作。如果一个类没有实现接口,当调用clone()方法时还会抛出异常CloneNotSupportException
public class B implements Cloneable{
private Long id;
private String name;
……
public Object clone() throw CloneNotSupportException{//定义的目的在于其他位置的调用,Object类中的clone方法时protected
return super.clone();
}
}
调用clone()方法进行浅克隆操作
B b = new B();//设置对应属性b1.setid(100L);
B b2 = (B)b1.clone(); //b2对象不同通过new创建的,而是克隆b1得到的对象
System.out.println(b2 == b1);//false,因为b1和b2不是同一个对象
如果需要深克隆,可以使用对象流实现,它会将所有相关的内容进行一次拷贝,而不会针对引用类型属性只是拷贝地址值。通过对象流实现对象的拷贝,则要求对象所属于的类必须实现Serializable接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEH8KBoN-1647049677799)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220218212047974.png)]
克隆方法的定义
public Object deepClone() {
Object res = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(this);
oos.flush();
ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
res = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (oos != null)
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (ois != null)
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return res;
}
测试
A a1=new A();
a1.setId(99L);
a1.setName("zhangsan");
a1.setBirth(new Date());
A a2=(A)a1.deepClone();
System.out.println(a1==a2);
System.out.println(a2);
//浅克隆
a2.getBirth().setYear(9000);
System.out.println(a1);
Set接口
扩展自Collection接口
-
顶级接口 Collection无序、允许重复
-
子接口List有序、允许重复
-
子接口Set无序,不允许重复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y7C6vEHT-1647049677801)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220218144205691.png)]
允许存储 null 值
没有新方法,只是对add、equals和hashcode方法添加了限制
Set接口的具体实现
HashSet和TreeSet是Set接口的具体实现,还有一个特殊的LinkedHashSet实现类
Set接口–>HashSet实现类–>LinkedHashSet实现类
Set接口–>SortedSet接口–>TreeSet实现类
HashSet的底层实现是HashMap,初始化后台容积(16、0.75)
- 存储数据的要求:首先调用存储对象的hashcode方法获取当前对象的hash值,然后如果hash值相等则出现覆盖
- 哈希表存储数据,具体的存储位置和对象的hash值相关,所以发现存储的数据是无序的
- LinkedHashSet就是在HashSet的基础上,后台维护一个允许于所有条目的链表,用于存储添加元素的顺序
树结构
树实际上就是一种抽象数据类型ADT或者实现这种抽象数据类型的数据结构,可以用于模拟具有树状结构性质的数据集合,是由n个有限节点构成的一个具有层次关系的集合。
树结构的特点:
- 每个节点都有有限个子节点或者无子节点
- 没有父节点的节点成为根节点
- 每个非根节点有且仅有一个父节点
- 除了根节点外,每个字节的可以分为多个互不相关的子树
- 树中没有环路cycle
引入数结构的目的在于结合有序数组和链表两种数据结构的优点。
二叉树
每个节点最多有两个分叉子节点,具体的存储方式有基于指针的链式存储和基于数组的顺序存储,最常见的是基于指针的方式,基于数组的方式比较浪费内存
满二叉树:每个节点都有左右两个子节点
完全二叉树:叶子节点分布在层次结构的最下面或者倒数第二层,最下面一层的叶子节点都靠左排列。除了最下
面一层,其他层结点都是饱满的,并且最下层上的结点都集中在该层最左边的若干位置上。(满二叉树也是完全二叉树)
基于指针的链式存储方式
public class Node{
private int data;//需要存储的数据
private Node left;//左子树
private Node right;//右子树
}
基于数组的链式存储方式
对于节点之间的父子关系,通过数组的下标计算得到对应的存储位置。例如节点 x 存储数组中下标为 i 的位置,那么下标为 2i 的位置存储的是它的左子节点;下标 2i+1 的位置存储的就是它的右子节点,下标为 i/2 的位置存储的就是它的父节点
二叉树的遍历方式有前序遍历、中序遍历和后序遍历三种方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qvOTRZvy-1647049677801)(C:\Users\阿白\AppData\Roaming\Typora\typora-user-images\image-20220218213708612.png)]
1、前序遍历:根–左--右
1,2,4,5,7,8,3,6
public void preOrder(Node root){
if(root==null) return;
System.out.println(root.data);
preOrder(root.left);
preOrder(root.right);
}
2、中序遍历:左–根--右
4,2,7,8,5,1,3,6
public void middleOrder(Node root){
if(root==null) return;
middleOrder(root.left);
System.out.println(root.data);
middleOrder(root.right);
}
3、后序遍历:左–右--根
4,8,7,5,2,6,3,1
public void postOrder(Node root){
if(root==null) return;
postOrder(root.left);
postOrder(root.right);
System.out.println(root.data);
}
排序二叉树
首先如果二叉树满足条件每个节点:左子树的所有节点值都小于它的根节点,而且所有右子树节点值大于它的根节点,这样的二叉树就是排序二叉树
折半查找算法
前提:要求有序数组
时间复杂度 O(logN)
public static int halfSearch(int[] array, int target) {
int min = 0;
int max = array.length - 1;
while (min <= max) {
int middle = (min + max) >> 1;
if (target > array[middle])
min = middle + 1;
else if (target < array[middle])
max = middle - 1;
else
return middle;
}
return -1;// 标识查找的元素不存在
}
注意:有序也应该是正序,否则上面的加减操作就会有问题
public static void main(String[] args) {
int[] arr = new int[11];
arr[0] = 300;
Random r = new Random();
for (int i = 1; i < arr.length; i++)
arr[i] = r.nextInt(1000);
// 使用折半查找的前提是目标数组有序--正序
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
int pos=halfSearch(arr, 300);
System.out.println(pos);
}
编码实现
类定义
private Node tree;// 树的根节点
public static class Node {
private int data;// 具体存储的数据
private Node left; // 左子树
private Node right;// 右子树
public Node(int data) {
this.data = data;
}
}
插入数据
首先从根节点开始向下查找自己需要插入数据的具体位置。具体流程:新节点和当前节点进行比较,如果相同则标识已经存在且不能重复插入;如果小于当前节点,则转向到左子树中接续查找,如果左子树为空,则当前节点为要查找的父节点,新节点插入到当前节点的左子树即可。如果大于当前节点,则到右子树中寻找,如果右子树为空则当前节点为要查找的父节点,新节点插入到当前节点的右子树即可。
public void insert(int data) {
if (tree == null) {
tree = new Node(data);
return;
}
Node p = tree;
while (p != null) {
if (data > p.data) {
if(p.right==null){
p.right=new Node(data);
return;
}
p=p.right;
} else {
if(p.left==null){
p.left=new Node(data);
return;
}
p=p.left;
}
}
}
删除操作
删除操作有 3 种情况:要删除的节点无子节点、要删除的节点只有一个子节点、要删除的节点有两个子节点
- 要删除的节点无子节点可以直接删除,就是让其父节点将该子节点置空即可
- 要删除的节点只有一个子节点,则替换要删除的节点为其子节点
- 要删除的节点有两个子节点,则首先查找该节点的替换节点(就是右子树中最小的节点或者左子树中的最大节点),接着替换要删除的节点为替换节点,然受删除替换节点
public void delete(int data){
Node p=tree;
Node pp=null; //pp用于指向p的父节点
while(p!=null && p.data!=data){
pp=p;
if(data>p.data) p=p.right;
else p=p.left;
}
if(p==null) return;
if(p.left!=null && p.right!=null){//要删除的节点有2个子节点
Node minp=p.right;
Node minpp=p;
while(minp.left!=null){
minpp=minp;
minp=minp.left;
}
p.data=minp.data;
p=minp;
pp=minpp;
}
Node child=null;
if(p.left!=null) child=p.left;
if(p.right!=null) child=p.right;
if(pp==null) tree=child;
else if(pp.left==p) pp.left=child;
else pp.right=child;
}
ght!=null){//要删除的节点有2个子节点
Node minp=p.right;
Node minpp=p;
while(minp.left!=null){
minpp=minp;
minp=minp.left;
}
p.data=minp.data;
p=minp;
pp=minpp;
}
Node child=null;
if(p.left!=null) child=p.left;
if(p.right!=null) child=p.right;
if(ppnull) tree=child;
else if(pp.leftp) pp.left=child;
else pp.right=child;
}