以下内容均来自于B站“动力节点之老杜java基础教程”
集合既容器(也是一个对象),一次可以容纳多个对象。集合中任何时候存储的都是“引用”!
java中不同的集合对应的数据结构是不一样的!如:数组,链表,二叉树,哈希表。。。
集合继承结构图:
Map
注意,map和collection是没有继承关系的,两者是相对独立的!
总结
以上图必须牢记!!!!
在使用了泛型之后,Collection中只能存储某个具体了类型!没有使用泛型之前,可以存储Object的所有子类型!集合中不能直接存储基本数据类型,也不能存java对象,只能存储java对象的内存地址!
import java.util.ArrayList; import java.util.Collection; public class CellectionTest01 { public static void main(String[] args) { //多态,父类型的引用,指向了子类型的对象!Collection只是一个集合类接口! Collection c=new ArrayList(); c.add(10);//自动装箱!实际上是放了一个对象的内存地址 c.add(3.14); c.add(new Object()); c.add(true); } }
常用方法
接口Collection继承了接口Iterable,从而也就继承了iterator()方法,当Collection调用该方法时,返回一个迭代器Iterator,目的就是迭代集合(遍历集合)!该迭代器适用于所有的集合(Map除外)!
import java.util.Collection; import java.util.HashSet; import java.util.Iterator; public class CellctionTest02 { public static void main(String[] args) { Collection c=new HashSet(); c.add("abc");c.add("def");c.add(100);c.add(new Object()); //使用迭代器 Iterator it=c.iterator(); //遍历 //迭代器判断指向的下一位元素是否还存在!boolean hasNext(); while(it.hasNext()){ //让迭代器前进一位,并将指向的元素拿到!Object next(); Object obj=it.next(); System.out.println(obj); } } }
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class CellectionTset03 { public static void main(String[] args) { //ArrayList特点:有序可重复(添加的顺序) Collection c=new ArrayList(); c.add(1);c.add(2);c.add(3);c.add(4);c.add(2); //遍历 Iterator it=c.iterator(); while(it.hasNext()){ Object ob=it.next();//让迭代器前进一位,并将指向的元素拿到!Object next(); System.out.println(ob); } } }
你存进去是什么类型,取出来还是什么类型,只是在sout的时候给你转化为字符串(调用toString方法)
import java.util.HashSet; import java.util.Collection; import java.util.Iterator; public class CollectionTest04 { public static void main(String[] args) { //HashSet特点:无序不可重复(添加的顺序) Collection c=new HashSet(); c.add(1);c.add(3);c.add(4);c.add(2);c.add(2);c.add(-5);c.add(9);c.add(7); //遍历 Iterator it=c.iterator(); while(it.hasNext()){ Object ob=it.next(); System.out.println(ob); } } }
import java.util.ArrayList; import java.util.Collection; public class CollectionTest05 { public static void main(String[] args) { Collection c=new ArrayList(); String s1="abc"; String s2="def"; c.add(s2);c.add(s1); String x=new String("abc"); System.out.println(c.contains(x)); } }
可能与我们想的不一样!因为s1和x两个对象虽然内容一样,但是对象地址不一样!
我们可以看到集合c不包含x,事实上也确实没有把x添加进去,那么为什么结果运行还是true呢,这个得看contains方法的底层源代码是怎么比的(既看是否有equals方法),通过查看底层源代码,发现使用o.equals(es[i])方法,与上对比其中x是o,es[i]是s1,既x.equals(s1)------->true?回答是true,因为String类中重写了equals方法,对比的是内容而不再是对象地址了!
测试2
import java.util.ArrayList; import java.util.Collection; public class CollectionTest06 { public static void main(String[] args) { Collection c=new ArrayList(); Student s1=new Student("jack"); Student s2=new Student("jack"); c.add(s1); System.out.println(c.contains(s2)); } } class Student{ private String name; Student(){} Student(String name){ this.name=name; } }
那为什么这个又是false呢?通过上面我们知道,contains的底层是通过调用equals方法来判断,又因为Student类没有重写,从而比较的是内存地址(s1.equals(s2)),当然是false了!
所以,我们重写Student中的equals方法,就能是运行结果为true了
//重写我们自己需要的equals方法,比较原理为内容相同 @Override public boolean equals(Object o) { if(o==null||!(o instanceof Student)) return false; if(o==this) return true; Student s=(Student)o; return s.name.equals(this.name); }
总结:放在集合中的元素,需要重写equals方法,不重写比较的对象的内存地址,重写了比较的是内容!
直接说结果,都不用敲代码演示了,remove方法的底层也是需要通过调用equals方法来比较的,所以你不重写equals方法,则比较的就是Object对象(既对象的内存地址),重写了就是另外一回事了!
先看一段代码:
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class CollectionTest07 { public static void main(String[] args) { Collection c=new ArrayList(); c.add("abc"); c.add("def"); c.add("ghi"); //通过迭代器,拿到一个元素删除一个元素 Iterator it=c.iterator(); while(it.hasNext()){ Object o=it.next(); c.remove(o);//删除拿到的元素在集合中 System.out.println(o); } } }
然后我们会发现报错,这是为什么呢?因为java规定,一旦集合的结构发生了改变,必须重新获取迭代器,否则便会出现异常!上面删除了元素,没有重新获取迭代器而是直接在原迭代器上直接操作,然后出现了异常!
怎么解决这种问题呢?使用迭代器自己的删除方法remove!
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class CollectionTest07 { public static void main(String[] args) { Collection c=new ArrayList(); c.add("abc"); c.add("def"); c.add("ghi"); Iterator it=c.iterator(); while(it.hasNext()){ Object o=it.next(); //最大的区别 it.remove();//删除的一定是迭代器自己的元素 System.out.println(o); } } }
第一个出错的原因是:集合中的元素删了,但是没有更新迭代器;第二个可以是因为通过迭代器去删除时,会自动更新迭代器,并且更新集合(既删除集合中的元素)!
List是Collection接口的子接口,特点是有序(有下标)可重复,我们着重学习List特有的方法
其中E代表泛型(后面会讲解),分别是向指定的索引下标(从0开始)添加元素、根据索引下标获取对应的元素、根据元素获取对应的索引下标(第一次出现)、获取指定元素最后一次出现处的索引、根据索引下标删除对应的元素、根据索引下标重新设置其对应的元素!
注意,要想使用以上方法,通过面向对象篇(多态)来熟悉以下两种方式的区别:
List list=new ArrayList<>();//可以 Collection c=new ArrayList<>();//不可以,无法使用List特有的方法,只能使用原Collection自有的方法
方法举例
import java.util.ArrayList; import java.util.List; import java.util.Iterator; public class ListTest01 { public static void main(String[] args) { List list=new ArrayList<>(); //对于ArrayList来说,add是在末尾添加 list.add("a");list.add("b");list.add("c"); Iterator it=list.iterator(); while(it.hasNext()){ Object o=it.next(); System.out.println(o); } System.out.println("===================="); list.add(1,"d");//一般不推荐,效率比较低,参考数据结构链表中元素的插入! //通过for循环来遍历 for (int i = 0; i <list.size() ; i++) { Object o=list.get(i); System.out.println(o); } } }
ps:
ArrayList的另一种构造方法
import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; public class test1 { public static void main(String[] args) { Collection c=new HashSet(); c.add(100);c.add(500);c.add(400);c.add(200); //通过这种构造方法,可以将HashSet集合转换为list集合 List list=new ArrayList(c); for (int i = 0; i <list.size() ; i++) { System.out.println(list.get(i)); } } }
注意,ArrayList是非线程安全的集合!!
在接触java自带的LinkedList之前,需要学习一下链表(单链表)这种数据结构
链表优点:随机增删元素效率较高(因为增删元素不涉及到大量的元素位移)
既在以后开发中遇到随机增删业务比较多建议用链表
链表缺点:查询效率低,每次查得从头节点开始往下遍历!
我们自己写一个单链表,来加深对其的理解(本质就是自己写数据结构):
/* 单链表中的节点 节点为单链表中的基本单元,包含"数据"和"下一个节点的内存地址"这两个属性 */ public class Node { Object element; Node next; public Node(){}; public Node(Object element,Node next){ this.element=element; this.next=next; } }
public class Link { Node header; int size=0; public int getSize(){ return size; } public void add(Object data){//增 if(header==null){ //假如没有头节点,则添加的这个既作为头节点 header=new Node(data,null); }else{//假如有头节点,找到最后一个节点,添加至其后 Node currentLastNode=findLast(header); currentLastNode.next=new Node(data,null); } size++; } //通过递归调用,创建一个寻找到链表尾节点的方法 public static Node findLast(Node node){ if(node.next==null){ return node; }else{ return findLast(node.next); } } public void remove(Object obj){//删 } public void modify(Object obj){//改 } public void find(Object obj){//查 } }
public class Test01 { public static void main(String[] args) { Link link=new Link(); link.add(100); link.add(200); link.add(300); System.out.println(link.getSize()); } }
需要注意的是,不是因为ArrayList有下标(linkedList也有下标)才检索快,而是因为ArrayList底层是数组,数组存放元素的地址是连续的,这才是ArrayList检索快的原因!而LinkedList即使有下标,也必须从头结点开始遍历一个一个的找!
那么,我们怎么将一个线程不安全的集合ArrayList转换成线程安全的集合呢?
import java.util.ArrayList; import java.util.List; //1,先导包(工具类) import java.util.Collections; public class Test01 { public static void main(String[] args) { List myList=new ArrayList();//非线程安全 //2,变成线程安全的(目前无法测试,强记) Collections.synchronizedList(myList); //下面的就是线程安全的了 myList.add(111); myList.add(222); myList.add(333); } }
不使用泛型:
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Test01 { public static void main(String[] args) { List myList=new ArrayList(); Dog b=new Dog(); Cat c=new Cat(); myList.add(b);myList.add(c); //遍历集合 Iterator it=myList.iterator(); while(it.hasNext()){ //不用泛型,迭代器取出的就只能是Object类 Object o=it.next(); //Animal a=it.next(); 编译无法通过 if(o instanceof Animals){ //Object中没有move方法,需要向下转型(强转,既高转低) Animals a=(Animals)o; a.move(); } } } } class Animals{ public void move(){ System.out.println("移动"); } } class Dog extends Animals{} class Cat extends Animals{}
使用泛型:
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Test02 { public static void main(String[] args) { //使用泛型List<Animals>后,表示myList集合中只允许存储Animals类型 //用泛型类指定集合中存储的集合类型 List<Animals> myList=new ArrayList<Animals>(); Dog b=new Dog(); Cat c=new Cat(); myList.add(b);myList.add(c); //遍历集合 Iterator<Animals> it=myList.iterator(); while(it.hasNext()){ //使用泛型之后,每次迭代返回的都是Animals类型 Animals a=it.next(); a.move(); } } } class Animals{ public void move(){ System.out.println("移动"); } } class Dog extends Animals {} class Cat extends Animals {}
泛型的好处:统一了类型;从集合中取出的元素是泛型指定的类型,因此无需进行大量的“向下转型”了!
泛型的缺点:集合中的元素缺乏多样性(事实上大多数情况下,集合中的元素类型都是统一的)
钻石表达式:
//ArrayList的<>中无需输入数据类型,系统自动推敲 List<Animals> myList=new ArrayList<>();
public class Test03<T>{ public void doSome(T t){ System.out.println(t); } public static void main(String[] args) { Test03<String> test=new Test03(); test.doSome("abc");//只能是String类型 } }
又叫增强for循环
import java.util.ArrayList; import java.util.List; public class Test04{ public static void main(String[] args) { List<String> list=new ArrayList<>(); list.add("abc"); list.add("def"); list.add("hij"); //使用foreach遍历集合 代码简介了很多 可以说增强for就是为泛型集合而生的 //当前缺点是没有下标,使用foreach时要慎重考虑这点 for (String s:list) { System.out.println(s); } } }
在上面最开始我们已经测试了,Set的特点是无序不可重复,不过需要注意的是:
放到HashSet中的元素,实际上是放到了HashMap集合的Key部分!
TreeSet虽然是无序不可重复,但是存储的元素可以按照大小自动排序,称为可排序集合
import java.util.Set; import java.util.TreeSet; //Set是无序不可重复的,无序是指没有下标,存放和取出的顺序是不一样的! public class TreeSet01 { public static void main(String[] args) { Set<String> strs=new TreeSet<>(); strs.add("A"); strs.add("B"); strs.add("Z"); strs.add("Y"); strs.add("Z"); strs.add("K"); strs.add("M"); //遍历 for (String str:strs) { System.out.println(str); } } }
常用方法:
import java.util.HashSet; import java.util.Set; public class Myclass { //声明一个静态内部类 public static class InnerClass{ public static void m1(){ System.out.println("静态内部类的m1方法执行"); } public void m2(){ System.out.println("静态内部类的实例方法m2执行"); } } public static void main(String[] args) { Myclass.InnerClass.m1(); //创建静态内部类对象 Myclass.InnerClass mi=new Myclass.InnerClass(); mi.m2(); //泛型(静态内部类) Set<Myclass.InnerClass> set=new HashSet<>(); Set<String> set2=new HashSet<>(); Set<MyMap.MyEntry<Integer,String>> set3=new HashSet<>(); } } class MyMap{ //使用泛型的静态内部类 public static class MyEntry<K,V>{ } }
public class MapTest01 { public static void main(String[] args) { Map<Integer,String> map=new HashMap<>(); map.put(1,"zhangsan"); map.put(2,"lisi"); map.put(3,"wamgwu"); map.put(4,"zhaoliu"); //通过key获取value String value=map.get(2); System.out.println(value); //获取键值对的数量 System.out.println("键值对的数量:"+map.size()); //通过key删除key-value map.remove(2); System.out.println("键值对的数量:"+map.size()); //注意,containsKey和containsValue底层都是用非equals进行比较的,需要重新equals方法! //判断是否包含某个key System.out.println("是否包含key=4:"+map.containsKey(4)); //判断是否包含某个value System.out.println("是否包含value=lishi:"+map.containsValue("lisi")); //获取所有的values Collection<String> str=map.values(); for (String s:str) { System.out.println(s); } //清空 map.clear(); System.out.println("Map是否为空:"+map.isEmpty()); } }
//遍历方式1,通过获取所有的key值来遍历 import java.util.HashMap; import java.util.Map; import java.util.Set; public class MapTest02 { public static void main(String[] args) { Map<Integer,String> map=new HashMap<>(); map.put(1,"zhangsan"); map.put(2,"lisi"); map.put(3,"wamgwu"); map.put(4,"zhaoliu"); Set<Integer> set =map.keySet(); for (Integer i:set) { System.out.println("key="+i+",values="+map.get(i)); } } }
/* * 第二种遍历方式:Set<Map.Entry<k,v>> entrySet * 这个是将Map集合全部转换成Set集合 * Set集合中元素的类型是:Map.Entry * */ import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class MapTest03 { public static void main(String[] args) { Map<Integer,String> map=new HashMap<>(); map.put(1,"zhangsan"); map.put(2,"lisi"); map.put(3,"wamgwu"); map.put(4,"zhaoliu"); Set<Map.Entry<Integer,String>> set=map.entrySet(); //遍历集合,每次取出一个node Iterator<Map.Entry<Integer,String>> it2=set.iterator(); while (it2.hasNext()){ //Set的类型就是Map.Entry<Integer,String> Map.Entry<Integer,String> node= it2.next(); Integer key=node.getKey(); String values= node.getValue(); System.out.println(key+"="+values); } /*使用foreach遍历 for (Map.Entry<Integer,String> node:set) { System.out.println(node.getKey()+"="+node.getValue()); } */ } }
下面只是图解,具体想了解可以去数据结构里学习!
import java.util.HashMap; import java.util.Map; import java.util.Set; public class HashMapTest01 { public static void main(String[] args) { //Integer是key,他的hashCode和equals都重写了 Map<Integer,String> map=new HashMap<>(); map.put(1111,"zhangsan"); map.put(6666,"lisi"); map.put(7777,"wangwu"); map.put(2222,"zhaoliu"); map.put(2222,"king"); System.out.println(map.size());//4,key重复的时候,value自动覆盖 Set<Map.Entry<Integer,String>> set= map.entrySet(); for (Map.Entry<Integer,String> node:set) { System.out.println(node.getKey()+"="+node.getValue()); } } }
HashMap的默认初始化时16,加载因子为0.75(底层数组的容量达到75%就开始扩容)
HashMap的初始化容量必须为2的倍数,这也是官方推荐的!
public class Student { private String name; public Student() {} public Student(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } //hashCode //equals 如果学生名字一样,表示同一个学生 public boolean equals(Object obj){ if(obj==null||!(obj instanceof Student)) return false; if(obj==this) return true; Student s=(Student)obj; return s.getName().equals(this.name); } }
import java.util.HashSet; import java.util.Set; public class HashMapTest02 { public static void main(String[] args) { Student s1=new Student("zhangsan"); Student s2=new Student("zhangsan"); //重写equal方法之后是true System.out.println(s1.equals(s2)); System.out.println("s1的hashCode:"+s1.hashCode()); System.out.println("s2的hashCode:"+s2.hashCode()); //由于上面的equals返回的是true,我们往hashSet集合中放,按理说只能放一个(HashSet:无序不可重复) Set<Student> set=new HashSet<>(); set.add(s1); set.add(s2); System.out.println(set.size()); } }
PS:向Map集合中存与取,都是先调用key的hashCode方法,再调用equals方法,equals有可能调用有可能不调用!
也就是说,向上面这样,数组下标为5的位置上为null既没有元素,则不会执行equals,直接插入就行了,否则像134这种都会执行equals方法来进行比较从而进行插入还是覆盖!
所以,上面测试二的执行结果是set.size()=2(原因上面也分析了,因为先执行hashCode,不同则插入)不符合HashSet集合的存储特点,
所以一个类的equals方法如果重写的话,也要同时且必须重写hashCode方法,既equals返回true,则hashCode方法的返回值必须一样!
好在这块儿不需要你自己去重写,直接通过IDEA自己的重写方法自动给你完成了!
你再运行测试二的代码:
终极结论:放在HashMap集合key部分的,以及放在HashSet集合中的元素,需要同时重写hashcode方法和equals方法!
最后再说一下,同一个链表上的Hash值可能不一样,因为有可能碰撞了,但转换的数组下标是一样的,这个其实也不要紧,因为会往链表下面添加;假如两个的hash值是一样的,肯定在同一链表上!
所谓只有一个是因为put方法如果有key值则会覆盖原value值
import java.util.Properties; public class PropertiesTest01 { public static void main(String[] args) { //创建一个Properties对象 Properties pro=new Properties(); //需要掌握的两个方法:一个存,一个取 pro.setProperty("101","zhangsan"); pro.setProperty("102","lisi"); pro.setProperty("103","wangwu"); pro.setProperty("104","zhaoliu"); //通过key值获取value String s1=pro.getProperty("101"); String s2=pro.getProperty("102"); String s3=pro.getProperty("103"); String s4=pro.getProperty("104"); //输出 System.out.println(s1); System.out.println(s2); System.out.println(s3); System.out.println(s4); } }
PS:建议把各个集合的初始容量和扩容倍数牢记(背),面试容易问!
public class TreaSetTest01 { public static void main(String[] args) { TreeSet<String> ts=new TreeSet<>(); ts.add("zhangsan"); ts.add("lisi"); ts.add("wnagwu"); ts.add("zhangsi"); for (String s:ts) { System.out.println(s); } } }
字符和数字是可以排序的,那么我们自定义的类型怎么办呢?这就需要该类型继承java.lang.Comparable接口,并且实现compareTo方法!
import java.util.TreeSet; public class TreeSetTest03 { public static void main(String[] args) { Customer c1=new Customer(32); Customer c2=new Customer(20); Customer c3=new Customer(30); Customer c4=new Customer(25); TreeSet<Customer> customer=new TreeSet<>(); customer.add(c1); customer.add(c2); customer.add(c3); customer.add(c4); for (Customer c:customer) { System.out.println(c); } } } //放在TreeSet集合中的元素需要继承java.lang.Comparable接口 //并且实现compareTo方法,equals可以不写 class Customer implements Comparable<Customer>{ int age; public Customer(int age) { this.age = age; } //编写比较的逻辑 @Override public int compareTo(Customer o) { int age1=this.age; int age2=o.age; if(age1==age2){ return 0; }else if(age1>age2){ return 1; }else { return -1; } } //重写toString @Override public String toString() { return "Customer[age="+this.age+"}"; } }
通过上面操作,成功达成了我们自定义类型的排序!像前面的Integer和String都能排是因为都实现了Comparable接口的方法,只不过是java帮我们自动实现了!
比较的规则底层利用了二叉树(感兴趣的可以自己查阅数据结构)
单独编写比较器
import java.util.Comparator; import java.util.TreeSet; //TreeSet的可排序的第二种方法,使用比较器 public class TreeSetTest06 { public static void main(String[] args) { //创建TreeSet集合的时候,需要使用这个比较器 //注意新的构造方法 TreeSet<Wugui> wuguis=new TreeSet<>(new WuguiComparator()); wuguis.add(new Wugui(100)); wuguis.add(new Wugui(800)); wuguis.add(new Wugui(300)); //遍历 for (Wugui w:wuguis) { System.out.println(w); } } } class Wugui{ int age; public Wugui(int age) { this.age = age; } @Override public String toString() { return "wugui{" + "age=" + age + '}'; } } //单独在这里编写一个比较器 //比较器实现java.util.Comparator接口(Comparable是java.lang包下的) class WuguiComparator implements Comparator<Wugui>{ //重写比较规则 @Override public int compare(Wugui o1, Wugui o2) { return o1.age-o2.age; } }
或者,为了简便,使用匿名内部类的方式!注意,这个类没有名字,后面直接new接口
TreeSet<Wugui> wuguis=new TreeSet<>(new WuguiComparator(){ //重写比较规则 @Override public int compare(Wugui o1, Wugui o2) { return o1.age-o2.age; } });
那么这两种方式怎么选择呢?
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class TreeSetTest07 { public static void main(String[] args) { List<String> list=new ArrayList<>(); //变成线程安全的 Collections.synchronizedList(list); //排序 list.add("abf"); list.add("abc"); list.add("abb"); list.add("abx"); //里面只能是list集合,且集合中的类型必须实现Comparable接口 //如果是Set集合,可以转换为List再使用Collections工具排序 Collections.sort(list); for(String s:list){ System.out.println(s); } } }