目录
在Java中可以通过数组保存多个对象,但某些情况下开发人员无法确定需要保存的数组长度,此时数组将不再适用,因为数组的长度是不变的。为了保存数目不确定的数据,JDK提供了一系列特殊的类,这些类可以存储任意类型的数据,并且长度可变,,在java中这些类统称为集合。集合类都位于java.util包中。
集合框架体系图
单列集合Collection:在添加数据的时候,一次只能添加一个数据。
双列集合Map:在添加数据的时候一次可以添加一对数据(键值对)。
List列表特点
有序集合(也称为序列 )。 该界面的用户可以精确控制列表中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。可以简单的理解成和数组的使用方式差不多 , 存储到List中的数据有顺序的 并且可以通过索引编号对其进行操作
ArrayList集合底层体现出来的数据结构是数组。对此集合的操作其实还是对数组的操作。
Linkedlist集合底层体现出来的数据结构是双向链表。链表中的每一个元素都是用引用的方式记住他的前一个与后一个元素,从而将所有元素链接起来
ArrayList增删效率低,查询效率高。
LinkedList增删效率高,查询效率低。
ArrayList存储结构![](https://i-blog.csdnimg.cn/blog_migrate/feefba690e96a2cab2c6ef8003f60e60.png)
LinkedList存储结构
ArrayList集合常用方法
LinkedList集合常用方法
Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,他有两个重要的子接口Set和List。
List特点是元素有序、可重复。在List集合中,允许出现重复元素,所有的元素都是以一种线性的方式进行存储的,在程序中可通过索引来访问集合中指定的元素,List集合另一特点是元素有序,即元素的存入顺序和取出顺序一致。主要实现类:ArrayList、LinkedList 。
Set特点元素无序、元素不可重复。主要实现类:HashSet、TreeSet.Set接口中的元素是无序的,并且会以为某种规则保证存入的元素不重复。
HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的。当向 HashSet 集合中添加一个对象时,首先会调用该对象的 hashCode ()方法来计算对象的哈希值,从而确定元素的存储位置。如果此时哈希值相同,再调用对象的 equals ()方法来确保该位置没有重复元素。 Set 集合与 List 集合存取元素的方式都一样(存取关键字基本一致)。
Map:双列集合的根接口,用于存储具有键(key)、值(value)映射关系的元素,每个元素都包含一对键值,可以通过指定的键找到对应得值。
Map接口的主要实现类:HashMap、TreeMap。
ArrayList与LinkedList区别
ArrayList:
ArrayList 是 List 接口的一个实现类,它是程序中最常见的一种集合。在 ArrayList 内部封装了一个长度可变的数组对象,当存入的元素超过数组长度时, ArrayList 会在内存中分配一个更大的数组来存储这些元素,因此可以将 ArrayList 集合看作一个长度可变的数组。 ArrayList 集合中大部分方法都是从父类 Colection 和 List 继承过来的,其中, add ()方法和 get ()方法用于实现元素的存取。
LinkedList:
ArrayList 集合在查询元素时速度很快,但在增删元素时效率较低。为了克服这种局限性,可以使用 List 接口的另一个实现类 LinkedList 。该集合内部维护了个双向循环链表,链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而可以将所有的元素彼此连接起来。当插入一个新元素时,只需要修改元素之间的这种引用关系即可,删除一个节点也是如此。正因为这样的存储结构, LinkedList 集合对于元素的增删操作具有很高的效率。
public class Test {
//集合框架
public static void main(String[] args) {
ArrayList a1 = new ArrayList();
a1.add(1);
a1.add("张三");
a1.add(3.14);
//在指定位置添加数据
a1.add(1,"李四");
ArrayList a2 = new ArrayList();
//再指定位置后一次添加多个
//a2.addAll(2,a1);
//一次添加多个
a2.addAll(a1);
a2.add(99);
//获取指定下标的元素
Object v = a2.get(2);
System.out.println(v);
//获取数组长度
int l = a2.size();
System.out.println(l);
//判断数据是否存在,返回布尔值
boolean b = a2.contains("李四");
System.out.println(b);
//寻找指定数据第一次出现的位置
int l2 = a2.indexOf("张三");
System.out.println(l2);
//删除指定数据
a2.remove("张三");
//删除指定下标的数据
// a2.remove(5);
//以字符串形式打印数组
System.out.println(a2.toString());
//清空数组
//a2.clear();
//更改数据
a2.set(1,"李五");
//获取数组长度
int c = a2.size();
System.out.println(c);
System.out.println(a2.isEmpty());
}
}
class Test2{
public static void main(String[] args) {
LinkedList l = new LinkedList();
l.add("张三");
l.add("张4");
l.add("张5");
l.add("张6");
l.add("张7");
l.add("张8");
l.add("张10");
LinkedList l2 = new LinkedList();
l2.addAll(l);
l2.add(6,"张9");
System.out.println(l2);
l2.addFirst("张2");
l2.addLast("张11");
System.out.println(l2);
//l2.remove("张三");
//l2.removeLast();
//l2.removeFirst();
l2.getFirst();
l2.getLast();
l2.indexOf("张四");
boolean b1 = l2.isEmpty();
boolean b2 = l2.contains("张8");
l2.peek();
l2.peekFirst();
l2.peekLast();
//System.out.println(e);
}
}
Iterator接口(迭代器)
此接口主要用于迭代访问(遍历)Collection中的元素,Collection与Map接口主要用于存储元素。Iterator仅用于遍历集合,本身并不存放对象。
JDK5.0新特性----foreach循环
虽然通过迭代器可遍历无下标的数组,但其写法繁琐,为了简化,JDK5.0开始提供foreach循环,又称增强for循环,专门用来遍历,不论有无下标都可以使用。与for循环相比,foreach循环不需要获取容器的长度,也不需要根据索引访问元素,但他会自动遍历容器中的每一个元素。每一次循环时,都通过变量记住当前循环的元素。foreach只能访问元素,不能修改。
public class Test {
public static void main(String[] args) {
HashSet h = new HashSet();//初始容器为空,默认大小16,负载因子为0.75
h.add("张0");
h.add("张1");
h.add("张1");
h.add("张2");
h.add("张3");
h.add("li3");
//使用foreach遍历
for(Object n : h){
System.out.println(n);
}
//迭代器遍历
//获取迭代器
Iterator iterator = h.iterator();
//判断指针是否能动 hasNext表示下一个元素,看看是否存在
while(iterator.hasNext()){
//取出指针指向的值
Object i = iterator.next();
//输出值
System.out.println(i);
}
}
}
迭代器遍历元素时,首先通过调用 ArrayList 集合的iterator0)方法获得选代器对象,然后使用 hasNext ()方法判断集合中是否存在下一个元素,如果存在,则调用 next ()方法将元素取出;否则说明已到达了集合末尾,停止遍历元素。需要注意的是,在通过 next ()方法获取元素时,必须保证要获取的元素存在,否则,会拋出 NoSuchElementException 异常。 lterator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素。
Set集合
Set特点元素无序、元素不可重复。主要实现类:HashSet、TreeSet。
Set 接口和 List 接ロ一样,同样继承自 Colection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Colection 接口更加严格了。与 List 接ロ不同的是, Set 接ロ中元素无序,并且都会以某种规则保证存入的元素不出现重复。 Set 接口主要有两个实现类,分别是 HashSet 和 TreeSet 。其中, HashSet 根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。 TreeSet 则是以二又树的方式来存储元素它可以实现对集合中的元素进行排序。接下来将对 HashSet 进行详细的讲解。
HashSet与TreeSet的区别
hashset:底层数据结构是哈希表,本质就是哈希值储存。通过判断元素的hashcode方法和equals方法来保证元素的唯一性。当哈希值不同时就直接进行储存。
Treeset:底层数据结构式一个二叉树,可以对set集合中的元素进行排序,这种结构,可以提高排序性能。根据比较方法的返回值决定的,只要返回的是0,就代表元素重复。hashset中存储的元素是无序的元素不可重复。
Treeset中存储的元素可以是有序的,元素可以自己设为可重复,可以自己设置排序规则。
二叉树:小的值存在左边,大的存在右边,相同的不存储
HashSet集合
HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的。当向 HashSet 集合中添加一个对象时,首先会调用该对象的hashCode0方法来计算对象的哈希值,从而确定元素的存储位置。如果此时哈希值相同,再调用对象的 equals (方法来确保该位置没有重复元素。 Set 集合与 List 集合存取元素的方式都一样,但无法使用get关键字获取指定元素。下面例子写不一致的地方。
public class Hash {
public static void main(String[] args) {
//创建HashSet对象,默认容器大小16,负载因子0.75
HashSet h = new HashSet();
//手动设置初始容器大小
HashSet h1 = new HashSet(10);
//设置初始容器大小,设置负载因子,
//负载因子为0.7f表示当空间使用超过70%时,会要求扩容。
HashSet h2 = new HashSet(10,0.7f);
h.add("张三");
h.add("张四");
h.add("张五");
h.add("张六");
h1.add("刘备");
h1.add("张飞");
h1.add("关羽");
h.addAll(h1);
System.out.println(h);
}
}
在HashSet中存入对象
HashSet里的对象可以和其他任意类型数据共存,而且各种类型数据也可以混在一起存。
//创建对象类
public class Student {
private String naen;
private int age;
public Student() {
}
public Student(String naen, int age) {
this.naen = naen;
this.age = age;
}
public void setNaen(String naen) {
this.naen = naen;
}
public void setAge(int age) {
this.age = age;
}
public void zwjs(){
System.out.println(this.age+""+this.naen);
}
//如果不重写toString方法,将默认输出地址
@Override
public String toString() {
return naen+" "+age;
}
}
class Test{
public void static main(String[] args){
HashSet h = new HashSet();
h.add(1);
h.add(1);
h.add(2);
h.add(23);
//新建Student对象
Student s = new Student("张三",10);
Student s1 = new Student("张4",120);
Student s2 = new Student("张5",10);
//将对象存入
h.add(s);
h.add(s1);
h.add(s2);
//直接存
h.add(new Student("tom",10));
System.out.println(h);
}
}
HashSet存储流程
![](https://i-blog.csdnimg.cn/blog_migrate/ee48f767f44e6bb1923bc58d570587bb.png)
TreeSet集合
- TreeSet中的方法和HashSet中的方法一模一样 只是他们的实现不一样。而且也没有get方法,要想取出元素只能用迭代器。
- TreeSet 基于TreeMap 实现。TreeSet可以实现有序集合,但是有序性需要通过比较器实现现。 但TreeSet集合在存入对象时会和HashSet有差别。
- TreeSet是实现Set接口的实现类。所以它存储的值是唯一的,同时也可以对存储的值进行排序,排序用的是二叉树原理。针对于二叉树,我们可以知道小的值存储在左边,大的值存储在右边,相等不存储。
- TreeSet属于集的范围,只能存放引用类型,不能用于基本数据类型它本身不能有重复的元素,当存入自定义的引用类型的时候就必须考虑到元素不可重复的这个特性,自定义引用类型类由自己定义,实现的接口的方法由自己实现,在用TreeSet存放元素的时候的排序规则由自己定义。
如果不想保证元素的唯一性,改一下compare方法就可以了,永远不要让它返回0。
存入TreeSet集合中的元素必须实现Comparable接口,并手动重写compareTo()方法。
有两种实现排序的方式:
1.自然顺序(Comparable)
- TreeSet类的add()方法中会把存入的对象提升为Comparable类型
- 调用对象的compareTo()方法和集合中的对象比较
- 根据compareTo()方法返回的结果进行存储
2.比较器顺序(Comparator)
- 创建TreeSet的时候可以指定一个Comparator
- 如果传入了Comparator的子类对象,那么TreeSet就会按照比较器中的顺序排序
- 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数
比较器
Comparable 是排序接口。
若一个类实现了Comparable接口,就意味着“该类本身支持排序”。此外,“实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。
接口中通过x.compareTo(y)来比较x和y的大小。若返回负数,意味着x比y小;返回零,意味着x等于y;返回正数,意味着x大于y。实现Comparable接口要覆盖compareTo方法, 在compareTo方法里面实现比较规则。
Comparator 是比较器接口。我们若需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口);那么,我们可以建立一个“该类的比较器”来进行排序。这个“比较器”只需要实现Comparator接口即可。也就是说,我们可以通过“实现Comparator类来新建一个比较器”,然后通过该比较器对类进行排序。实现Comparator需要覆盖 compare 方法,在compare方法里面实现比较规则。
两者的联系
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
底层源码可参考此文章
数据结构 - TreeSet - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/126171944
TreeSet存对象的错误方式
Student类使用HashSet中的。
public class TreeSetClass {
public static void main(String[] args) {
TreeSet t = new TreeSet();
t.add("张1");
t.add("张2");
t.add("张3");
t.add("张4");
t.add("张5");
t.add("张6");
t.add("张7");
Student s = new Student("张三",10);
Student s1 = new Student("张4",120);
Student s2 = new Student("张5",10);
//存入对象
t.add(s);
System.out.println(t);
}
}
这种方式会报如下错误
TreeSet中放对象必的话须实现Comparable接口方可放入TreeSet。
正确方式
//调用Comparable接口
public class Student implements Comparable {
String naen;
int age;
public Student() {
}
public Student(String naen, int age) {
this.naen = naen;
this.age = age;
}
//如果不重写toString方法,将默认输出地址
@Override
public String toString() {
return naen + " " + age;
}
//当等于的时候就返回0,当大于就返回1,当小于就返回-1。
@Override
public int compareTo(Object o) {
//将传入的对象转成Student型。
Student s = (Student) o;
if (this.age > s.age) {
return 1;
}
if (this.age < s.age) {
return -1;
}
return 0;
}
}
public class TreeSetClass {
public static void main(String[] args) {
TreeSet t = new TreeSet();
t.add(new Student("张3",11));
t.add(new Student("张4",12));
t.add(new Student("张5",11));
t.add(new Student("张6",14));
System.out.println(t);
}
}
结果,由于我们定义的是按年龄比较,所以相同年龄的不会存入
Map集合
双列集合的根接口,他的每个元素都包含一个键对象(key)和值对象(value), 如果往map中添加元素时需要添加key 和 value,键和值之间存在一种对应关系,称为映射。从map集合中访问元素时,可以通过指定的键找到对应的值。
Map的键(key)必须唯一,值(value)可以重复.
Map接口的主要实现类:HashMap、TreeMap。
HashMap
常用方法
put与putIfAbsent区别
1.使用put方法添加键值对,如果map集合中没有该key对应的值,则直接添加,并返回null,如果已经存在对应的值,则会覆盖旧值,value为新的值。
2.使用putIfAbsent方法添加键值对,如果map集合中没有该key对应的值,则直接添加,并返回null,如果已经存在对应的值,则依旧为原来的值 。
public class Test {
public static void main(String[] args) {
//创建HashMap对象
//默认初始大小为16,负载因子为0.75
Map m = new HashMap();
//指定容器大小
Map m2 = new HashMap(10);
//指定容器大小和负载因子
Map m3 = new HashMap(10,0.75f);
//添加
m.put("name1","张三");
m.put("name2","张四");
m.put("name3","张四");
m.put("name4","张四");
m2.put("name5","李四");
m2.put("name6","李五");
m2.put("name7","李六");
m.putAll(m2);
m.putIfAbsent("name1","tom");
m.putIfAbsent("name9","tom");
System.out.println(m);
//删除
m.remove("name9");
m2.clear();
//修改
m.replace("name3","张5");
System.out.println(m);
//查询
boolean b = m.containsKey("name1");
boolean b2 = m.containsKey("name");
boolean b3 = m.containsValue("张四");
System.out.println(b+" "+b2+" "+b3);
Object v = m.get("name1");
System.out.println(v);
Set s = m.keySet();
System.out.println(s);
//遍历
for(Object o : m.keySet()){
Object val = m.get(o);
System.out.println(o+">>"+val);
}
}
}
HashMap存储原理
哈希冲突
根据key的hash计算出相应得哈希值,根据相应的算法求出该元素在数组中得位置, 如果求出得哈希值与已有值相同,则称为哈希冲突
HashMap的存储底层在Java1.7和Java1.8的区别
JDK1.7使用得数据结构: 数组+链表 而且链表插入模式为头部插入(造成死循环)。
JDK1.8使用得数据结构: 数组+链表+红黑树 而且链表得插入模式为尾部插入。
泛型
集合可以存任意类型的对象,但当把一个对象存入集合后,集合会“忘记”这个对象的类型,将该对象从集合中取出时,会被编译成Object类型,换句话说就是在程序中无法确定集合中的元素到底是什么类型,那么在取出元素时,要进行强制类型转换就很容易出错,而泛型就是为了解决这个问题。在创建集合类时,使用泛型规定该集合类中存储的对象的类型。
泛型的类型只能是引用类型,比如String ,Integer,基础的四类八种不能用,包装类可以用。
泛型的使用:
list类定义泛型:集合类<泛型类型> 集合类名 = new 集合类();
map类定义泛型:集合类<key的泛型类型,value的泛型类型> 集合类名 = new 集合类();
ArrayList<String> i = new ArrayList<String>();此集合类中只能存String类型的元素。
jdk1.7以后new后的<String>可省去。
ArrayList<String> i2 = new ArrayList();
HashMap<String,Integer> h = new HashMap<>();
public class Test {
public static void main(String[] args) {
//list集合类定义泛型
ArrayList<String> i = new ArrayList<String>();
//jdk1.7以后new后的<String>可省去。
ArrayList<String> i2 = new ArrayList();
List <Integer> i1 = new ArrayList<>();
//map集合类定义泛型
HashMap<String,Integer> h = new HashMap<>();
h.put("name1",20);
}
}
自定义泛型
public class Demo {
public static void main(String[] args) {
Point<Integer> p = new Point<>();
p.setX(10);
p.setY(20);
p.print();
Point p1 = new Point();
}
}
//<T> 表示我们传入的泛型类型
class Point<T>{
private T x;
private T y;
public void print(){
System.out.println("x坐标"+x+"y坐标"+y);
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
}