1、概念
容器用于装其他对象的对象。
数组是一种简单的容器,优点:效率高、速度快,缺点:使用不够灵活,不能扩容,因此需要功能更加强大的容器,如下容器(集合)接口。
2、Collection集合的常用操作
基础方法:
remove()方法,仅移除容器中记录的对象的地址,并不是删除对象本身,新手容易犯错。
clear()方法,移除容器的所有内容。
toArray()方法,转化为一个object数组。
contains()方法,检查是否包含某个对象。
两个集合的方法:
removeAll()方法,移除A、C的交集元素。
retainAll()方法,保存A、C的交集元素。
3、List接口
List是有序可重复的容器。
有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
可重复:List允许加入重复的元素。更确切地讲,List通常允许满足e1.equals(e2)的元素重复加入容器。(Set接口则是无序不可重复的方法)
List接口常用实现类有3个:ArrayList(主要使用)、LinkedList、Vector等
4、ArrayList接口
ArrayList底层是用数组实现的存储。特点:查询效率高,增删效率低(LinkList),线程不安全(Vector)。我们一般使用它。
底层实现:
数组长度是有限的,而ArrayList是可以存放任意数量的对象,长度不受限制,那么它是怎么实现的呢?使用数组扩容实现,定义更长的数组,将旧内容拷贝到数组前部分,新内容放到旧内容后面,如下:
add()方法中,若就数组不够长度,那么方法调用链中用于数组扩容部分的代码如下:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;//保存旧长度
int newCapacity = oldCapacity + (oldCapacity >> 1);//新长度=旧长度+旧长度右移一位(即除以2)
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);//将旧数组拷贝一次,放入以新长度new出来的数组中,将新数组引用返回给原引用对象
}
remove()方法代码如下:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);//从旧数组被移除位置后一个开始,将后面的数据全部copy到旧数组从被移除位置开始,那么被移除位置被覆盖
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
5、手写ArrayList
以下包含手写的ArrayList、泛型、数组扩容、边界检查、移除元素、重写toString()
package net.com.hyh._20201128ArrayLisy_manual;
/***
* hyh
* 01自定义实现一个ArrayList
* 02增加泛型
* 03增加数组扩容
* 04增加数组边界检
* 05增加删除
* */
public class SxtArrayList05<E> {
private Object [] elementData;//核心数组存储数据
private int size;
private static final int DEFALT_CAPACITY=10;//定义默认长度
public SxtArrayList05(){
elementData=new Object[DEFALT_CAPACITY];//无参构造,默认长度
}
public SxtArrayList05(int capacity){
//检查索引
if(capacity<0)//不合法,手动抛出异常
throw new RuntimeException();
elementData=new Object[capacity];//有参构造,给定长度
}
//add方法,添加元素
//数组扩容
public void add(E element){
//什么时候扩容
if(elementData.length==size)
{
//怎么扩容
//新数组长度=旧数组长度+旧数组长度右移1位,即减半,注:“+”优先级高于“>>”,因此elementData.length>>1要加括号
Object[] newArray=new Object[elementData.length+(elementData.length>>1)];
//数组拷贝,将原数组从0开始,拷贝到新数组从0开始,拷贝原数组所有内容
System.arraycopy(elementData,0,newArray,0,elementData.length);
//新数组赋给原引用对象,旧数组空间被垃圾回收
elementData=newArray;
}
elementData[size++]=element;
}
//重写toString方法
@Override
public String toString() {
StringBuilder sb=new StringBuilder();//临时存储数组
sb.append("[");
for(int i=0;i<size;i++){
sb.append(elementData[i]+",");
}
sb.setCharAt(sb.length()-1,']');//替换最后的“,”
return sb.toString();
}
//索引检查方法
private void checkRange(int index){
//检查索引
if(index<0||index>=size)//不合法,手动抛出异常
throw new RuntimeException();
}
//获得元素
public E get(int index){
checkRange(index);
return (E)elementData[index];
}
//设置元素
public void set(E element,int index){
checkRange(index);
elementData[index]=element;
}
//按对象移除
public void remove(E element){
for(int i=0;i<size;i++)
if(elementData[i].equals(element))
{
remove(i);//全部移除
}
}
//按下标移除
public void remove(int index){
checkRange(index);//检查是否越界
int moveLength=elementData.length-index-1;//获得需要移动部分的长度
if(moveLength>0) {//若需要移除最后一个元素,则不copy,直接将容器最后一位赋值为null并减小容器长度
System.arraycopy(elementData, index + 1, elementData, index, moveLength);//对自身copy
}
elementData[--size]=null;
}
//测试
public static void main(String[] args) {
SxtArrayList05 s1=new SxtArrayList05(20);
for(int i=0;i<40;i++){
s1.add("elm"+i);
}
s1.set("1",1);
System.out.println(s1);//默认调用toSting方法
//
//System.out.println(s1.get(1));
//System.out.println(s1.get(40));
s1.remove(39);
System.out.println(s1);//默认调用toSting方法
s1.remove(1);
System.out.println(s1);//默认调用toSting方法
s1.remove("elm4");
System.out.println(s1);//默认调用toSting方法
}
}
6、手写LinkedList(查询效率低,不常用)
LinkedList底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。 所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。结构如下图(下图有误,不带头结点,且不循环):
定义:
public class Node {
Node pre;
Node next;
Object element;
}
以下包括LinkedList的get方法、检查方法、Remove方法、插入结点方法、泛型等。
package net.com.hyh._20201201ArrayList_manual;
/**
* 定义一个链表
*添加get、检查方法
*增加Remove
*添加插入结点
*增加泛型
*
*/
public class MyLinkList04<E> {
private Node first;//头指针
private Node last;//尾指针
private int size;
public MyLinkList04() {//构造函数,带头结点的链表
}
private void checkRange(int index){
if(index<0||index>=size){
throw new RuntimeException("索引数字不合法:"+index);
}
}
//添加元素
//["a","b","c"]
public void add(E element){
Node node=new Node(element);
if(first==null){
first=node;
last=node;
}
else {
node.pre = last;
node.next = null;
last.next = node;
last = node;
}
size++;
}
@Override
public String toString() {
Node temp=first;
StringBuilder sb=new StringBuilder();
sb.append("[");
while (temp!=null){
sb.append(temp.element+",");
temp=temp.next;//注意:指针不能忘了后移
}
sb.setCharAt(sb.length()-1,']');
return sb.toString();
// Node temp=first;
// while (temp!=null){
// System.out.println(temp.element);
// temp=temp.next;
// }
// return "";
}
//["a","b","c","d","e","f","g"]
public E get(int index){
Node temp=getNode(index);
return temp!=null?(E)temp.element:null;
}
private Node getNode(int index)
{
checkRange(index);
Node temp=null;
if(index<(size>>1)){//size>>1相当于除以2
temp =first;
for(int i=0;i<index;i++)
{
temp=temp.next;
}
}
else{//若查找位置在链表后半段,则从后向前找
temp =last;
for(int i=size-1;i>index;i--)
temp=temp.pre;
}
return temp;
}
public void remove(int index)
{
Node temp=getNode(index);
if(temp!=null) {
if (temp == first) {
first = first.next;
first.pre = null;
} else if (temp == last) {
last = last.pre;
last.next = null;
} else {
temp.pre.next = temp.next;
temp.next.pre = temp.pre;
}
}
size--;
}
public void insert(Object obj,int index)
{
Node temp=getNode(index);
if(temp!=null) {
Node newNode=new Node(obj);
if (temp == first)
{
newNode.next=first;
first.pre=newNode;
first=newNode;
}
else{
newNode.next=temp;
newNode.pre=temp.pre;
newNode.pre.next=newNode;
temp.pre=newNode;
}
}
}
//测试
public static void main(String[] args) {
MyLinkList04<String> l1=new MyLinkList04<>();
l1.add("1");
l1.add("2");
l1.add("3");
l1.add("4");
l1.add("5");
l1.add("6");
l1.remove(5);
l1.insert("5",0);
System.out.println(l1);
}
}
7、Vector向量(线程安全,效率低,不常用)
Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。 比如,indexOf方法就增加了synchronized同步标记(多线程方法)。
如何选用ArrayList、LinkedList、Vector?
1)需要线程安全时,用Vector。
2)不存在线程安全问题时,并且查找较多用ArrayList(一般使用它)。
3)不存在线程安全问题时,增加或删除元素较多用LinkedList。
8、Map接口
(1)概念
Map就是用来存储“键(key)-值(value) 对”的。 Map类中存储的“键值对”通过键来标识,所以“键对象”不能重复。(调用equals方法比较,重复则覆盖)
常用方法:
HashMap底层实现采用了哈希表,这是一种非常重要的数据结构。
数据结构中由数组和链表来实现对数据的存储,他们各有特点。
(1) 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。
(2) 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。
那么,我们能不能结合数组和链表的优点(即查询快,增删效率也高)呢? 答案就是“哈希表”。 哈希表的本质就是“数组+链表”。
存储示意图:
JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。
(2)手动实现HashMap
以下包括put方法、get方法、toString方法、泛型
package net.com.hyh._20201203;
import java.util.Arrays;
/**
* 自定义HashMap
* put方法
* toString方法
*泛型
*/
public class MyHashArrayNode<K,V> {
MyHashListNode[] table;
int size;
public MyHashArrayNode() {
this.table = new MyHashListNode[16];
}
public void put(K key,V value){
//若要再完整一点,考虑数组扩容
MyHashListNode newNode=new MyHashListNode();
newNode.hash=myHash(key.hashCode(),table.length);
newNode.key=key;
newNode.value=value;
newNode.next=null;
MyHashListNode temp=table[newNode.hash];
if(temp==null) {//若此处数组元素为空,则直接将新结点放进去
table[newNode.hash]=newNode;
size++;
}
else {//若此处结点不为空,则遍历链表
MyHashListNode lastNode=null;
while (temp!=null){
if(temp.key.equals(newNode.key))//key重复,覆盖
{
temp.value=newNode.value;
break;//找到重复值,结束循环
}
else {//不重复,后移
lastNode=temp;
temp = temp.next;
}
}
if(temp==null) {//链表遍历完毕没有重复值
lastNode.next = newNode;
size++;
}
}
}
public int myHash(int v,int length){
//System.out.println("&:"+(v&(length-1)));//直接位运算,效率高 10100
//System.out.println("%:"+(v%(length-1)));//取模运算,效率低 01111
return v&(length-1); // 00100 = 4
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder("{");//存储遍历到的数据
for (int i=0;i<table.length;i++)//先遍历数组
{
MyHashListNode temp=table[i];
while (temp!=null){//遍历链表
sb.append(temp.key+":"+temp.value+",");
temp=temp.next;
}
}
sb.setCharAt(sb.length()-1,'}');
return sb.toString();
}
public V get(K key){//根据key对象获得对应的value值
int hash=myHash(key.hashCode(),table.length);//获得散列值
MyHashListNode temp=table[hash];
while (temp!=null){
if(temp.key.equals(key)) {//相等则知道了key对象
return (V) temp.value;
}
else
temp=temp.next;
}
return null;
}
public static void main(String[] args) {
MyHashArrayNode<Integer,String > m=new MyHashArrayNode<>();
m.put(10,"a");
m.put(20,"b");
m.put(20,"b0");
m.put(53,"b1");
m.put(69,"b2");
m.put(85,"b3");
System.out.println(m);
// for(int i=0;i<100;i++)
// System.out.println(i+"->"+m.myHash(i,16));
System.out.println(m.get(53));
}
}
(3)TreeMap
TreeMap是红黑二叉树的典型实现。我们打开TreeMap的源码,发现里面有一行核心代码:
private transient Entry<K,V> root = null; |
root用来存储整个树的根节点。我们继续跟踪Entry(是TreeMap的内部类)的代码:
图9-23 Entry底层源码
可以看到里面存储了本身数据、左节点、右节点、父节点、以及节点颜色。 TreeMap的put()/remove()方法大量使用了红黑树的理论。本书限于篇幅,不再展开。需要了解更深入的,可以参考专门的数据结构书籍。
TreeMap和HashMap实现了同样的接口Map,因此,用法对于调用者来说没有区别。HashMap效率高于TreeMap;在需要排序的Map时才选用TreeMap。
9、Set接口
Set接口继承自Collection,Set接口中没有新增方法,方法和Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。因此,学习Set的使用将没有任何难度。
Set容器特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set中某个元素通过equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null元素,不能多个。
Set常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。
list容器特点:有顺序,可重复
(1)HashSet
HashSet是采用哈希算法实现,底层实际是用HashMap实现的(HashSet本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。
我们发现里面有个map属性,这就是HashSet的核心秘密。我们再看add()方法,发现增加一个元素说白了就是在map中增加一个键值对,键对象就是这个元素,值对象是名为PRESENT的Object对象。说白了,就是“往set中加入元素,本质就是把这个元素作为key加入到了内部的map中”。
(2)TressSet
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
使用TreeSet要点:
(1) 由于是二叉树,需要对元素做内部排序。 如果要放入TreeSet中的类没有实现Comparable接口,则会抛出异常:java.lang.ClassCastException。
(2) TreeSet中不能放入null元素。
10、迭代器
迭代器为提供了统一的遍历容器的方式。
以下包含List、Set、Map的遍历方式
package net.com.hyh._20201207;
import java.util.*;
public class TestIterator {
public static void main(String[] args) {
//testIeratorList();
//testIeratorSet();
testIeratorMap();
}
public static void testIeratorList()
{
List<String> list=new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
//使用iterator遍历list
for(Iterator<String>iter=list.iterator();iter.hasNext();)
{
//System.out.println(iter.hasNext());
String temp=iter.next();//返回对象的同时将指针后移
System.out.println(temp);
}
}
public static void testIeratorSet()
{
Set<String> set=new HashSet<>();
set.add("aa");
set.add("bb");
set.add("cc");
//使用iterator遍历set
for(Iterator<String>iter=set.iterator();iter.hasNext();)
{
//System.out.println(iter.hasNext());
String temp=iter.next();//返回对象的同时将指针后移
System.out.println(temp);
}
}
public static void testIeratorMap()
{
Map<Integer,String> map=new HashMap<>();
map.put(100,"aa");
map.put(13,"bb");
map.put(12,"cc");
//第一种遍历Map方式
//使用iterator遍历map,先将键值对存入set,再遍历set
Set<Map.Entry<Integer,String>> ss=map.entrySet();
for(Iterator<Map.Entry<Integer,String>> iter = ss.iterator(); iter.hasNext();)
{
//System.out.println(iter.hasNext());
Map.Entry<Integer,String> temp=iter.next();//返回对象的同时将指针后移
System.out.println(temp.getKey()+"--"+temp.getValue());
}
//第二种遍历Map方式
Set<Integer> keySet=map.keySet();
for(Iterator<Integer> iter=keySet.iterator();iter.hasNext();)
{
//System.out.println(iter.hasNext());
Integer key=iter.next();//返回对象的同时将指针后移
System.out.println(key+"--"+map.get(key));
}
}
}
11、容器遍历方法总结
【示例9-15】遍历List方法一:普通for循环
1 2 3 4 | for(int i=0;i<list.size();i++){//list为集合的对象名 String temp = (String)list.get(i); System.out.println(temp); } |
【示例9-16】遍历List方法二:增强for循环(使用泛型!)
1 2 3 | for (String temp : list) { System.out.println(temp); } |
【示例9-17】遍历List方法三:使用Iterator迭代器(1)
1 2 3 4 | for(Iterator iter= list.iterator();iter.hasNext();){ String temp = (String)iter.next(); System.out.println(temp); } |
【示例9-18】遍历List方法四:使用Iterator迭代器(2)
1 2 3 4 5 6 | Iterator iter =list.iterator(); while(iter.hasNext()){ Object obj = iter.next(); iter.remove();//如果要遍历时,删除集合中的元素,建议使用这种方式! System.out.println(obj); } |
【示例9-19】遍历Set方法一:增强for循环
1 2 3 | for(String temp:set){ System.out.println(temp); } |
【示例9-20】遍历Set方法二:使用Iterator迭代器
1 2 3 4 | for(Iterator iter = set.iterator();iter.hasNext();){ String temp = (String)iter.next(); System.out.println(temp); } |
【示例9-21】遍历Map方法一:根据key获取value
1 2 3 4 5 | Map<Integer, Man> maps = new HashMap<Integer, Man>(); Set<Integer> keySet = maps.keySet(); for(Integer id : keySet){ System.out.println(maps.get(id).name); } |
【示例9-22】遍历Map方法二:使用entrySet
1 2 3 4 | Set<Entry<Integer, Man>> ss = maps.entrySet(); for (Iterator iterator = ss.iterator(); iterator.hasNext();) { Entry e = (Entry) iterator.next(); System.out.println(e.getKey()+"--"+e.getValue()); |
12、Collection工具类
类 java.util.Collections 提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。
1. void sort(List) //对List容器内的元素排序,排序的规则是按照升序进行排序。
2. void shuffle(List) //对List容器内的元素进行随机排列。
3. void reverse(List) //对List容器内的元素进行逆续排列 。
4. void fill(List, Object) //用一个特定的对象重写整个List容器。
5. int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。
public class Test {
public static void main(String[] args) {
List<String> aList = new ArrayList<String>();
for (int i = 0; i < 5; i++){
aList.add("a" + i);
}
System.out.println(aList);
Collections.shuffle(aList); // 随机排列
System.out.println(aList);
Collections.reverse(aList); // 逆序
System.out.println(aList);
Collections.sort(aList); // 排序
System.out.println(aList);
System.out.println(Collections.binarySearch(aList, "a2"));
Collections.fill(aList, "hello");
System.out.println(aList);
}
}
【示例9-23】Collections工具类的常用方法
执行结果如图9-31所示:
13、用Map和List存储表格
每一行数据使用一个Map。
整个表格使用一个List。
ORM思想:对象关系映射(将表格映射成对象)
package net.com.hyh._20201207;
import java.util.*;
public class TestStoreData {
public static void main(String[] args) {
Map<String,Object> row1=new HashMap<>();
row1.put("ID",1001);
row1.put("姓名","张三");
row1.put("Salary",20000);
row1.put("入职日期",20201102);
Map<String,Object> row2=new HashMap<>();
row2.put("ID",1002);
row2.put("姓名","李四");
row2.put("Salary",10000);
row2.put("入职日期",20201101);
Map<String,Object> row3=new HashMap<>();
row3.put("ID",1003);
row3.put("姓名","王五");
row3.put("Salary",30000);
row3.put("入职日期",20201103);
List<Map<String,Object>> table=new ArrayList<>();
table.add(row1);
table.add(row2);
table.add(row3);
// for(Map<String,Object> row:table)
// {
// System.out.println(row);
// }
for(Map<String,Object> row:table)
{
Set<String> keyset=row.keySet();//获得所有的键
for(String key:keyset)//挨个打印每个键的值
System.out.print(key+":"+row.get(key)+"\t");
System.out.println();
}
}
}
13、用JavaBe和List存储表格
每一行数据使用一个javabean对象。
整个表格使用一个List/Map。
ORM思想:对象关系映射(将表格映射成对象)
package net.com.hyh._20201207;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TestStoreData2 {
public static void main(String[] args) {
User user1=new User(1001,"张三",10000,"20201101");
User user2=new User(1002,"李四",20000,"20201102");
User user3=new User(1003,"王五",30000,"20201103");
//map存储表格
Map<Integer,User> table1=new HashMap<>();
table1.put(1,user1);
table1.put(2,user2);
table1.put(3,user3);
//list存储表格
List<User> table2=new ArrayList<>();
table2.add(user1);
table2.add(user2);
table2.add(user3);
//直接打印Map中的数据,keySet可以全部存入到Set对象中再遍历取值
System.out.println("table1:");
for(Integer key:table1.keySet())
System.out.println(
"id="+table1.get(key).getId()+
",name=\'"+table1.get(key).getName()+"\'"+
",salary="+table1.get(key).getSalary()+
",hiredate=\'"+table1.get(key).getHiredate()+"\'");
//重写toString打印List中的数据
System.out.println("table2:");
for (User u:table2)
System.out.println(u.toString());
}
}
class User{
private int id;
private String name;
private double salary;
private String hiredate;
@Override
public String toString() {
return "id=" + id +
", name='" + name + '\'' +
", salary=" + salary +
", hiredate='" + hiredate + '\'';
}
//一个完整的Javabean要有set和get方法,以及无参构造器
public User() {
}
public User(int id, String name, double salary, String hiredate) {
this.id = id;
this.name = name;
this.salary = salary;
this.hiredate = hiredate;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setSalary(double salary) {
this.salary = salary;
}
public void setHiredate(String hiredate) {
this.hiredate = hiredate;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public String getHiredate() {
return hiredate;
}
}