文章目录
一、List系列集合
1.1 List集合的常用方法
以下是相对于父类增加的方法:
相较于父类增加的多是含索引的方法(add、remove)
代码演示:
//1.创建一个ArrayList集合对象(有序、有索引、可以重复)
List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("至尊宝");
list.add("牛夫人");
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]
//2.public void add(int index, E element): 在某个索引位置插入元素
list.add(2, "紫霞仙子");
System.out.println(list); //[蜘蛛精, 至尊宝, 紫霞仙子, 至尊宝, 牛夫人]
//3.public E remove(int index): 根据索引删除元素, 返回被删除的元素
System.out.println(list.remove(2)); //紫霞仙子
System.out.println(list);//[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]
//4.public E get(int index): 返回集合中指定位置的元素
System.out.println(list.get(3));
//5.public E set(int index, E e): 修改索引位置处的元素,修改后,会返回原数据
System.out.println(list.set(3,"牛魔王")); //牛夫人
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛魔王]
1.2 List集合的遍历方式
List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("糖宝宝");
//1.普通for循环
for(int i = 0; i< list.size(); i++){
//i = 0, 1, 2
String e = list.get(i);
System.out.println(e);
}
//2.增强for遍历
for(String s : list){
System.out.println(s);
}
//3.迭代器遍历
Iterator<String> it = list.iterator();
while(it.hasNext()){
String s = it.next();
System.out.println(s);
}
//list相较于set多了列表迭代器的方法(ListIterator)
//该迭代器相较于collection多了边遍历边添加的方法
//和倒序遍历的方法
//4.lambda表达式遍历
list.forEach(s->System.out.println(s));
1.3 ArryList底层原理
ArrayList集合底层是基于数组结构实现的,也就是说当你往集合容器中存储元素时,底层本质上是往数组中存储元素。
1.4 LinkedList底层原理
LinkedList集合是基于双向链表实现了,所以相对于ArrayList新增了一些可以针对头尾进行操作的方法
底层代码实现:
二、Set系列集合
该集合在内存中存储时是一个散列,一般是由一个哈希值来确定存储的位置。
对于Set集合的应用:
//Set<Integer> set = new HashSet<>(); //无序、无索引、不重复
//Set<Integer> set = new LinkedHashSet<>(); //有序、无索引、不重复
Set<Integer> set = new TreeSet<>(); //可排序(升序)、无索引、不重复
set.add(666);
set.add(555);
set.add(555);
set.add(888);
set.add(888);
set.add(777);
set.add(777);
System.out.println(set); //[555, 666, 777, 888]
//当前treeset对数据自动排序且不重复
2.1 HashSet集合底层原理
HashSet集合底层是基于哈希表实现的
- JDK8以前:哈希表 = 数组+链表
- JDK8以后:哈希表 = 数组+链表+红黑树’
几点疑问:
- 哈希表为什么是无序的:其根据哈希值存储,不像List是利用栈或队列存储
- 哈希表为什么没有索引:哈希表无法正常索引,在哈希值下还可能有复杂的红黑树链表结构
- 哈希表去重原理:利用hashCode和equals方法结合使用
2.2 HashSet去重原理
HashSet存储元素去重依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。
我们一般在要存储的对象中会重写这两个方法
public class Student{
private String name; //姓名
private int age; //年龄
private double height; //身高
//无参数构造方法
public Student(){}
//全参数构造方法
public Student(String name, int age, double height){
this.name=name;
this.age=age;
this.height=height;
}
//...get、set、toString()方法自己补上..
//按快捷键生成hashCode和equals方法
//alt+insert 选择 hashCode and equals
//equals方法用来比较两对象的属性值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false;
if (Double.compare(student.height, height) != 0) return false;
return name != null ? name.equals(student.name) : student.name == null;
}
//hashCode方法用来生成两对象的hash值
/*/在生成的过程中首先用name来得到一个初始的hash值,然后乘质数31,之后转化为一个长整型。
@Override
public int hashCode() {
int result;
long temp;
result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
temp = Double.doubleToLongBits(height);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
对应的测试类:
public class Test{
public static void main(String[] args){
Set<Student> students = new HashSet<>();
Student s1 = new Student("至尊宝",20, 169.6);
Student s2 = new Student("蜘蛛精",23, 169.6);
Student s3 = new Student("蜘蛛精",23, 169.6);
Student s4 = new Student("牛魔王",48, 169.6);
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
for(Student s : students){
System.out.println(s);
}
}
}
2.3 LinkedHashSet底层原理
LinkedHashSet与hashSet集合方法相同,是其子类
LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:
该类解决了HashSet不能记录存储顺序的问题,但是也在存储时带来更多的开销
2.4 TreeSet集合
treeSet类相比于父类没有新的方法
Set<Integer> set1= new TreeSet<>();
set1.add(8);
set1.add(6);
set1.add(4);
set1.add(3);
set1.add(7);
set1.add(1);
set1.add(5);
set1.add(2);
System.out.println(set1); //[1,2,3,4,5,6,7,8]
Set<Integer> set2= new TreeSet<>();
set2.add("a");
set2.add("c");
set2.add("e");
set2.add("b");
set2.add("d");
set2.add("f");
set2.add("g");
System.out.println(set1); //[a,b,c,d,e,f,g]
2.5 TreeSet排序出错
当排序自己定义的对象时,集合需要自己指定排序规则根据特定的属性排序。
此时有两种办法可以解决该问题:
- 第一种:让元素的类实现Comparable接口,重写compareTo方法,在使用add方法时直接调用比较方法。
- 第二种:在创建TreeSet集合时,通过构造方法传递Compartor比较器对象
方式一:
//第一步:先让Student类,实现Comparable接口
//注意:Student类的对象是作为TreeSet集合的元素的
public class Student implements Comparable<Student>{
private String name;
private int age;
private double height;
//无参数构造方法
public Student(){}
//全参数构造方法
public Student(String name, int age, double height){
this.name=name;
this.age=age;
this.height=height;
}
//...get、set、toString()方法自己补上..
//第二步:重写compareTo方法
//按照年龄进行比较,只需要在方法中让this.age和o.age相减就可以。
/*
原理:
在往TreeSet集合中添加元素时,add方法底层会调用compareTo方法,根据该方法的
结果是正数、负数、或是零,决定元素放在后面、前面还是不存在。
*/
@Override
public int compareTo(Student o) {
//this:表示将要添加进去的Student对象
//o: 表示集合中已有的Student对象
return this.age-o.age;
}
}
方式二:
//创建TreeSet集合时,传递比较器对象排序
/*
原理:当调用add方法时,底层会先用比较器,根据Comparator的compare方是正数、负数、或是零,决定谁在后,谁在前。
*/
//下面代码中是按照学生的年龄升序排序
Set<Student> students = new TreeSet<>(new Comparator<Student>{
@Override
public int compare(Student o1, Student o2){
//需求:按照学生的身高排序
return Double.compare(o1.age,o2.age);
}
});
//创建4个Student对象
Student s1 = new Student("至尊宝",20, 169.6);
Student s2 = new Student("紫霞",23, 169.8);
Student s3 = new Student("蜘蛛精",23, 169.6);
Student s4 = new Student("牛魔王",48, 169.6);
//添加Studnet对象到集合
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
System.out.println(students);
三、泛型
3.1 泛型的简介
所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:< E >),称为泛型类、泛型接口、泛型方法、它们统称为泛型。
该方法一般针对引用数据类型使用
ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>
表示元素的数据类型。
当没有泛型约束时:
从上图中可见在集合中可以添加任意类型的数据,此时的数据类型是以Object类的形式存入
-
泛型的好处:在编译阶段可以避免出现一些非法的数据。
-
泛型的本质:把具体的数据类型传递给类型变量。
3.2 泛型的相关细节
- 指定泛型数据后,在传入数据时,可以向该其中传入该类型或其子类
- java中的泛型是一个**伪泛型,在实际存储时也是存储Object类(基本数据类型无法转为Object类存储)
3.3 泛型类
一般需要自己定义的泛型类非常少,多是要阅读别人写好的代码
定义格式:
//这里的<T,W>其实指的就是类型变量,可以是一个,也可以是多个。
public class 类名<T,W>{
}
可以自己定义一个MyArrayList泛型类,模拟一下自定义泛型类的使用。
//定义一个泛型类,用来表示一个容器
//容器中存储的数据,它的类型用<E>先代替用着,等调用者来确认<E>的具体类型。
public class MyArrayList<E>{
private Object[] array = new Object[10];
//定一个索引,方便对数组进行操作
private int index;
//添加元素
public void add(E e){
array[index]=e;
index++;
}
//获取元素
public E get(int index){
return (E)array[index];
}
}
在另一个文件中再写一个类
public class Test{
public static void main(String[] args){
//1.确定MyArrayList集合中,元素类型为String类型
MyArrayList<String> list = new MyArrayList<>();
//此时添加元素时,只能添加String类型
list.add("张三");
list.add("李四");
//2.确定MyArrayList集合中,元素类型为Integer类型
MyArrayList<Integer> list1 = new MyArrayList<>();
//此时添加元素时,只能添加String类型
list.add(100);
list.add(200);
}
}
==可见泛型类中的泛型格式一般在类的实例化时在尖括号中说明的 ==
3.4 泛型方法
泛型方法与泛型类的区别:
- 泛型类中申明的泛型定义跟在类名后,类中的所有方法都可以使用该泛型
- 泛型方法的泛型声明在方法的返回值类型之前的位置,只有该方法可以使用
定义格式:
public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){
若返回值只有泛型,可以省略返回值类型的书写
}
代码演示:
public class Test{
public static void main(String[] args){
//调用test方法,传递字符串数据,那么test方法的泛型就是String类型
String rs = test("test");
//调用test方法,传递Dog对象,那么test方法的泛型就是Dog类型
Dog d = test(new Dog());
}
//这是一个泛型方法<T>表示一个不确定的数据类型,由调用者确定
public static <T> T test(T t){
return t;
}
}
public static <E> void addAll(ArryList<E> arry,E e){
}
可见泛型方法中定义的泛型种类一般由调用时的参数来确定
3.5 泛型接口
泛型接口其实指的是在接口中把不确定的数据类型用
<类型变量>
表示。定义格式类似于泛型类的定义。
//这里的类型变量,一般是一个字母,比如<E>
public interface 接口名<类型变量>{
}
代码演示:
public interface Data<T>{
public void add(T t);
public ArrayList<T> getByName(String name);
}
//此时确定Data<E>中的E为Teacher类型,
//接口中add和getByName方法上的T也都会变成Teacher类型
public class TeacherData implements Data<Teacher>{
public void add(Teacher t){
}
public ArrayList<Teacher> getByName(String name){
}
}
若子类中仍不能确定泛型的类型可继续用< E >
可见泛型接口中的泛型类型确定,是在接口实现时在尖括号中说明的
3.6 泛型的继承和通配符
泛型不具备继承性,但是数据可以继承,即泛型不能实现多态用父类接收子类对象 ,但父类的泛型可以存子类的数据。
可以使用通配符来约束
class Ye {
}
class Fu extends Ye{
}
class Zi extends Fu {
}
public class GemericTest1 {
public static void main(String[] args) {
ArrayList<Ye> list1=new ArrayList<>();
ArrayList<Fu> list2=new ArrayList<>();
ArrayList<Zi> list3=new ArrayList<>();
//不含通配符的方法
method(list1);
//method(list2);//报错
//形参是父类容器,不能传递子类容器,无法实现多态
list1.add(new Ye());
list1.add(new Fu());
//父类的容器可以装子类的对象
method2(list1);
method2(list2);
method3(list1);
method3(list2);
method4(list1);
}
//不含通配符
public static void method(ArrayList<Ye> list) {}
//通配符?可以接收任何类型的数据
public static void method2(ArrayList<?> list){}
//通配符? extends 类名 只能接收该类机器子类
public static void method3(ArrayList<? extends Ye> list){}
//通配符? super 类名 只能接收该类及其父类
public static void method4(ArrayList<? super Zi> list){}
}
总结:父类的泛型容器形参不能接子类的泛型容器,但父类的容器能存子类对象