紫薇星上的Java——类集

这篇文章来整理一下类集的知识点。


类集框架


类集框架简介

从JDK1.2开始Java引入了类集开发框架,所谓类集指的就是动态对象数组的实现方案,在实际开发中没有任何一项开发可以离开数组,但是传统的数组实现起来非常的繁琐,而且长度是致命伤,因为长度的问题传统的数组是不可能大范围使用的,但是开发又不能离开数组,最初就只能依靠一些数据结构动态的实现数组处理,而其中最为重要的两个结构:链表、树,面对与这些数据结构的实现又不得不面对如下的困难:

  • 数据结构的代码实现困难,对于一般的开发者是无法进行使用的;
  • 对于链表或者二叉树进行修改更新时进行维护是非常麻烦的;
  • 对于链表或二叉树还需要尽可能保证其操作的性能;

正是因为这样的原因,从JDK1.2开始Java引入了类集,主要就是对常见的数据结构进行完整的实现包装,它提供了一系列的接口与实现子类来帮助用户减少数据结构说带来的开发困难。

最初的类集实现由于Java本身的技术所限制,对于数据的控制并不严格,全部采用了Object的类型进行了数据接收,而在JDK1.5之后由于泛型技术的推广,所以类集本身也得到了改进,可以直接利用泛型来保存相同类型的数据,并且随着数据量的不断增加,从JDK1.8开始类集中的实现算法也得到了良好的性能提升。

在整个类集框架中提供有如下几个核心接口:Collection、List、Set、Map、Iterator、Enumeration、Queue、ListIterator。


Collection接口简介

java.util.Collection是单值集合操作的最大父接口,在该接口之中定义有所有的单值数据的处理,这个接口之中定义有如下的核心操作方法:

  • 向集合保存数据(单个数据):public boolean add(E e);
  • 追加一组数据:public boolean addAll(Collection<? extends E> c);
  • 清空集合,让根节点为空,同时执行GC处理:public void clear();
  • 查询数据是否存在,需要equals()方法支持:public boolean contains(Object o);
  • 数据删除,需要equals()方法支持:public boolean remove(Object o);
  • 获取数据长度,最大不得超过Integer.MAX_VALUE:public int size();
  • 将集合变为对象数组返回:public  Object[] toArray();
  • 将集合变为Iterator接口:public Iterator<E> iterator();

其中add()方法与iterator()方法占到了90%的使用率,也就是数据增加和数据输出。

在JDK1.5的版本之前Collection只是一个独立的接口,但是从JDK1.5之后提供了Iterable父接口,并且在JDK1.8子哦胡针对于Iterable接口进行了一些扩充,另外在JDK1.2-JDK1.4的时代中如果要进行集合的使用往往会直接操作Collection的接口,但是从JDK1.5时代开始,更多的情况下选择的都是Collection的两个子接口:允许重复的List子接口、不允许重复的Set子接口。


List集合


List接口简介

List是Collection的子接口,其最大的特点是允许保存有重复的元素数据,该接口的定义如下:

public interface List<E> extends Collection<E>

但要清楚List接口对于Collection接口进行了方法扩充:

  • 获取指定索引上的数据:public E get(int index);
  • 修改指定索引对象的数据:public E set(int index, E element);
  • 返回ListIterator接口对象:public ListIterator<E> listIterator();

在List的使用中,get()方法绝对是使用率最高的一个方法,但List本身依然属于一个接口,对于接口要想使用就必须有一个子类来完成定义,在List子接口中有三个常用子类:ArrayList、Vector、LinkedList,大部分时间使用ArrayList,其次是LinkedList,而Vector是使用率最少的。

从JDK1.9开始,List子接口中追加有一些static方法以方便用户的处理:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = List.of("Hello","World","123456","紫薇星");
		System.out.println(all);
	}
}
[Hello, World, 123456, 紫薇星]

同时可以看到all这个对象在代码中是一个List集合对象,而Collection是List的父接口,Collection可以将all变为对象数组:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = List.of("Hello","World","123456","紫薇星");
		Object result [] = all.toArray();
		for(Object temp : result) {
			System.out.println(temp);
		}
	}
}
Hello
World
123456
紫薇星

这些操作功能原本是不存在的,并不是List的传统用法,而是在新版本之后添加的新功能。


ArrayList子类

ArrayList是List子接口使用最多的一个子类,但这个子类在使用的时候也是有前提要求的,我们来对其相关定义及源代码组成进行分析,在Java中ArrayList类的定义如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList的继承结构如下: 

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		all.add("hello");
		all.add("world");
		all.add("hello");
		System.out.println(all);
	}
}

我们使用ArrayList来是是实现List接口,向里面添加重复数据,输出:

[hello, world, hello]

通过本程序可以发现List的存储特征:

  • 保存的顺序就是其存储顺序;
  • List的集合中允许存在重复数据;

在以上的程序中虽然实现了集合的输出,但这种输出操作是直接利用了每个类的toString()方法实现的,为了方便的进行输出处理,在JDK1.8之后Iterable父接口之中定义有一个forEach()方法,定义如下:

  • 输出支持:default void forEach(Consumer<? super T> action);
public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		all.add("hello");
		all.add("world");
		all.add("hello");
		all.forEach((str)->{
			System.out.println(str + ",");
		});
	}
}
hello,
world,
hello,

这种输出形式是JDK1.8之后才有的,但这种输出并不是正常开发下要考虑的操作形式。

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		System.out.println("是否为空?" + all.isEmpty() + "个数:" + all.size());
		all.add("hello");
		all.add("world");
		all.add("hello");
		System.out.println("是否为空?" + all.isEmpty() + "个数:" + all.size());
		all.remove("hello");
		all.forEach((str)->{
			System.out.println(str + ",");
		});
	}
}
是否为空?true个数:0
是否为空?false个数:3
world,
hello,

简单使用一下ArrayList的方法,可以发现ArrayList的操作支持与之前编写的链表的形式是非常相似的,但它并不是通过链表来实现的,通过类名称就可以发现,ArrayList应该封装的是一个数组。

ArrayList的无参构造方法:

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

对于这个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA在文档中可以找到:private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

还有一个有参构造方法:

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

通过有参构造方法可以发现在ArrayList中所包含的数据就是一个对象数组:transient Object[] elementData; 而还有一个默认值为:private static final int DEFAULT_CAPACITY = 10;

如果现在在进行数据追加的时候发现ArrayList集合里面保存的对象数组长度不够的时候,会进行新的数组开辟并将原始的旧数组内容拷贝到新数组之中:

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

如果在实例化ArrayList对象的手并没有传递初始化的长度,则默认情况下会使用一个空数组,但如果进行数据增加的时候发现数组容量不够了,则会判断当前增长的容量与默认的容量大小后使用一个较大的数值进行新的数组开辟,所以可以得出结论:

  • 在JDK1.9之后,ArrayList默认构造只会使用空的数组,使用的时候才会开辟新数组,而默认开辟长度为10;
  • 在JDK1.9之前,ArrayList默认的构造就会默认开辟大小为10的数组;

可以看到新数组为int newCapacity = oldCapacity + (oldCapacity >> 1);这说明当ArrayList中保存的容量不足的时候会采用成倍增长的方式,原始长度为10,则下次增长为20,下次为40,以此类推。 

使用ArrayList的时候一定要估算出数据量,如果超过了10个就使用有参构造方法进行创建,以避免垃圾数组空间的产生。


ArrayList保存自定义类对象

之前的分析已经清楚了ArrayList子类的实现原理以及List核心操作,但是在使用时用的是系统提供的String类,这是一个设计非常完善的类,而对于类集而言也可以实现自定义类对象的保存:

class Person{
	private String name;
	private int age;
	public Person() {
		super();
	}
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<Person> all = new ArrayList<Person>();
		all.add(new Person("zhangsan", 20));
		all.add(new Person("lisi", 30));
		all.add(new Person("wangwu", 40));
		all.forEach(System.out::println); //方法引用代替了消费性的接口
	}
}
Person [name=zhangsan, age=20]
Person [name=lisi, age=30]
Person [name=wangwu, age=40]

这时候我们来加点操作:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<Person> all = new ArrayList<Person>();
		all.add(new Person("zhangsan", 20));
		all.add(new Person("lisi", 30));
		all.add(new Person("wangwu", 40));
		System.out.println(all.contains(new Person("lisi", 30)));
		all.remove(new Person("lisi", 30));
		all.forEach(System.out::println);
	}
}
false
Person [name=zhangsan, age=20]
Person [name=lisi, age=30]
Person [name=wangwu, age=40]

会发现,查找也查找不到,删除也删除不了,我们要知道:在使用List保存自定义的类对象的时候如果需要使用到contains()和remove()方法进行查询与删除,就要保证已经成功的覆写了equals()方法:

@Override
	public boolean equals(Object obj) {
		if(this == obj) {
			return true;
		}
		if(obj == null) {
			return false;
		}
		if(!(obj instanceof Person)) {
			return false;
		}
		Person per = (Person)obj;
		return this.name.equals(per.name) && this.age == per.age;
	}

这时再运行程序,就可以实现操作了:

true
Person [name=zhangsan, age=20]
Person [name=wangwu, age=40]

LinkedList子类

在List接口中还有一个比较常用的子类:LinkedList,这个类通过名称就已经可以发现特点了:基于链表的实现。

还是先来看一下LinkedList的定义:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList的子类继承关系如下:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new LinkedList<String>();
		all.add("zhangsan");
		all.add("zhangsan");
		all.add("lisi");
		all.add("wangwu");
		all.forEach(System.out::println);
	}
}
zhangsan
zhangsan
lisi
wangwu

如果现在只是观察程序的功能,会发现和ArrayList使用时完全一样,但是其内部的机制是完全不一样的。

LinkedList中的构造方法并没有像ArrayList那样提供初始化大小的方法,而只是提供有无参的构造处理:public LinkedList();

接下来我们观察add()方法的实现:

public boolean add(E e) {
        linkLast(e);
        return true;
    }

在之前编写自定义链表的时候,判断了传入数据是否为null,如果为null则不进行保存,但在LinkedList中并没有做这样的处理,而是所有的数据都可以保存,而后此方法调用了linkLast()方法:

void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

LinkedList类中保存的数据都是通过Node结点进行的封装处理,同时为了提高程序的执行的性能,每次都会保存上一个(最后一个)追加的结点来避免递归处理,在增加数据的时候要保证size增加,也就是数据保存个数的增加。

通过一系列分析之后就会发现,LinkedList封装的就是一个链表实现。

ArrayList与LinkedList有什么区别?

  • ArrayList属于数组实现的集合操作,而LinkedList属于链表实现的集合操作;
  • 在使用List集合中的get()方法根据索引获取数据时,ArrayList的时间复杂度为“O(1)”,而LinkedList的时间复杂度为“O(n)”;
  • ArrayList在使用的时候默认的初始化对象数组的大小为10,空间不足则会采用翻倍的处理形式进行容量的扩充,如果保存大数据量的时候可能会产生垃圾空间的产生或性能的下降,这时候可以使用LinkedList来保存;

Vector子类

Vector是一个原始而又古老的程序类,这个类是在JDK1.0的时候提供的,到了JDK1.2的时候,由于许多的开发者已经习惯使用Vector了,并且许多的系统类也是基于Vector实现的,考虑到使用的广泛性,所以类集框架将其保存了下来并且多实现了一个List接口:

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

看到它的第一反应就是和ArrayList很相似: 

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new Vector<String>();
		all.add("zhangsan");
		all.add("zhangsan");
		all.add("lisi");
		all.add("wangwu");
		all.forEach(System.out::println);
	}
}
zhangsan
zhangsan
lisi
wangwu

它的用法也和ArrayList差不多,我们来看一下它的构造:

public Vector() {
        this(10);
    }

public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

如果使用的是无参的构造方法,这一定会开辟一个10个长度的数组,而后其余的实现操作与ArrayList操作相同,通过源代码发现Vector类中的操作方法都采用的是synchronized同步处理,而ArrayList并没有同步处理,所以在多线程访问的时候Vector属于线程安全的,但是性不如ArrayList高。


Set集合

Set集合最大的特点就是不允许保存重复元素,也是Collection的子接口。


Set接口简介

在JDK1.9以前,Set集合与Collection集合的定义并无差别,也就是说Set继续使用了Collection接口中提供的方法进行操作,但在JDK1.9之后,Set集合也像List集合一样扩充了一些static方法,Set集合的定义如下:

public interface Set<E> extends Collection<E>

要注意的是Set集合并不像List集合一样扩充了很多新方法,所以无法使用List集合中提供的get()方法,也就是说无法实现指定索引数据的获取,Set接口的继承关系如下:

从JDK1.9之后,Set集合也提供了像List集合之中类似的of()静态方法:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<String> all = Set.of("hello","hello","world");
		System.out.println(all);
	}
}

这里我们故意设置了重复内容,看一下运行结果:

Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: hello
	at java.base/java.util.ImmutableCollections$SetN.<init>(ImmutableCollections.java:463)
	at java.base/java.util.Set.of(Set.java:501)
	at com.test.TestDemo.main(TestDemo.java:16)

直接告诉我们有重复元素 hello,当使用of()这个新方法的时候如果发现集合之中存在有重复元素这会直接抛出异常,这与传统的Set集合不保留重复元素的特点一致,只不过自己跑出了异常而已。

Set集合的常规使用形式一定是依靠子类进行实例化的,所以Set接口之中有两个常用的子类:HashSet、TreeSet。


HashSet子类

HashSet是Set接口中使用最多的子类,其最大的特点就是保存的数据是无序的,而HashSet子类的继承关系如下:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

这种继承的形式和之前的ArrayList是非常相似的:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<String> all = new HashSet<String>();
		all.add("zhangsan");
		all.add("zhangsan");
		all.add("lisi");
		all.add("wangwu");
		all.forEach(System.out::println);
	}
}
lisi
zhangsan
wangwu

通过执行结果就可以发现HashSet子类的操作特点:

  • 不允许保存重复元素(由Set接口定义的);
  • 保存的数据是无序的;

TreeSet子类

Set接口的另外一个子类就是TreeSet,与HashSet最大的区别在于TreeSet中保存的数据是有序的,来看一下TreeSet的继承结构关系:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<String> all = new TreeSet<String>();
		all.add("zhangsan");
		all.add("zhangsan");
		all.add("lisi");
		all.add("wangwu");
		all.forEach(System.out::println);
	}
}
lisi
wangwu
zhangsan

当利用TreeSet保存数据的时候所有的数据都会按照数据的升序来进行自动排序处理。


分析TreeSet子类排序操作

经过分析之后发现TreeSet子类之中保存的数据是允许排序的,接下来会使用一个自定义的类来实现排序的处理操作

class Person{
	private String name;
	private int age;
	public Person() {
		super();
	}
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<Person> all = new TreeSet<Person>();
		all.add(new Person("zhangsan", 20));
		all.add(new Person("lisi", 30));
		all.add(new Person("wangwu", 40));
		all.add(new Person("wangwu", 40));
		all.forEach(System.out::println); //方法引用代替了消费性的接口
	}
}

运行一下,会发现报错了:

Exception in thread "main" java.lang.ClassCastException: com.test.Person cannot be cast to java.base/java.lang.Comparable
	at java.base/java.util.TreeMap.compare(TreeMap.java:1291)
	at java.base/java.util.TreeMap.put(TreeMap.java:536)
	at java.base/java.util.TreeSet.add(TreeSet.java:255)
	at com.test.TestDemo.main(TestDemo.java:48)

现在我们知道了,TreeSet子类允许数据排序,但这个类必须实现Comparable接口,因为只有实现了此接口才能够确认出对象的大小关系。TreeSet本质上是利用了TreeMap子类实现的集合数据存储,而TreeMap需要根据Comparable来确定大小关系。

class Person implements Comparable<Person>{
	private String name;
	private int age;
	public Person() {
		super();
	}
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	@Override
	public int compareTo(Person per) {
		return this.age - per.age;
	}
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<Person> all = new TreeSet<Person>();
		all.add(new Person("lisi", 20));
		all.add(new Person("zhangsan", 20));
		all.add(new Person("lisi", 30));
		all.add(new Person("wangwu", 40));
		all.add(new Person("wangwu", 40));
		all.forEach(System.out::println); //方法引用代替了消费性的接口
	}
}

我们实现Comparable接口后再添加几个数据,这时候注意我噩梦的数据有两个是年龄相同的:

Person [name=lisi, age=20]
Person [name=lisi, age=30]
Person [name=wangwu, age=40]

这时候会发现年龄相同的时候会将上一个删除,这是因为我们只判断了年龄,来修改一下:

@Override
	public int compareTo(Person per) {
		if(this.age < per.age) {
			return -1;
		}else if(this.age > per.age) {
			return 1;
		}else {
			return this.name.compareTo(per.name);
		}
	}
Person [name=lisi, age=20]
Person [name=zhangsan, age=20]
Person [name=lisi, age=30]
Person [name=wangwu, age=40]

这个结果告诉我们要使用自定义类进行对象的比较处理的时候一定要将该类中的所有属性依次按照大小关系进行匹配,否则有一个或者几个属性相同的时候也会认为是重复数据,所以得出结论:TreeSet是利用Comparable接口来确认重复数据的。

由于TreeSet在操作过程中需要将所有的属性进行比对,实现难度太高,在实际开发之中应该首选HashSet子类来进行存储。


分析重复元素消除

TreeSet子类是利用了Comparable接口来实现了重复元素的判断,但是Set集合的整体特征就是不允许保存重复元素,但是HashSet判断重复元素并不是利用Comparable接口完成的,而是利用Object类中提供的方法实现的:

  • 对象编码:public native int hashCode();
  • 对象比较:public boolean equals(Object obj);

在进行重复元素比较的时候首先利用hashCode()进行编码的匹配,如果该编码不存在就表示数据不存在,证明没有重复,如果存在就进一步进行对象比较处理,如果重复了此数据是不允许存储的。

而再ecplice中可以直接生成hashCode()和equals()方法,就在Source中:

@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (age != other.age)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
Person [name=wangwu, age=40]
Person [name=lisi, age=20]
Person [name=lisi, age=30]
Person [name=zhangsan, age=20]

再Java程序之中真正的重复元素判断处理利用的就是hashCode()和equals()方法共同作用完成的,而只有在TreeSet这种排序要求下才会要求Comparable接口来实现。


集合输出

实际上从JDK1.8开始就在Iterator的接口中提供有一个forEach()方法,但这种输出形式并不是传统意义上的集合输出形式,并且也很难在实际开发之中出现,对于集合操作而言,一共定义有四种输出形式:Iterator迭代输出、ListIterator双向输出、Enumeration枚举输出、foreach输出,大多数情况下使用Iterator输出,少部分情况使用Enumeration,而foreach与Iterator相当。


Iterator迭代输出

通过Collection接口的继承关系可以发现,从JDK1.5开始多继承了一个Iterator的父接口,在这个接口中定义了一个iterator()方法,通过此方法可以获取Iterator接口对象(在JDK1.5之前,这一方法直接定义在Collection接口之中):

  • 获取Iterator接口对象:public Iterator<T> iterator();

在Iterator接口中定义有如下方法:

  • 判断是否有数据:public boolean hasNext();
  • 取出当前数据:public E next();
  • 删除:public default void remove();

在之前使用的java.util.Scanner类就是Iterator的接口子类,所以此时类继承关系如下:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<String> all = Set.of("aaa","bbb","ccc","ddd");
		Iterator<String> iter = all.iterator(); //实例化iterator对象
		while(iter.hasNext()) {
			String str = iter.next();
			System.out.println(str);
		}
	}
}
ccc
bbb
aaa
ddd

但是对于Iterator接口中的remove()方法的使用需要特别注意一下,如果不是必须不要使用,在Collection接口中定义有数据的删除方法,但在进行迭代输出的过程中如果使用了Collection中的remove()方法会导致迭代失败:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<String> all = new HashSet<String>();
		all.add("aaa");
		all.add("bbb");
		all.add("ccc");
		all.add("ddd");
		Iterator<String> iter = all.iterator(); //实例化iterator对象
		while(iter.hasNext()) {
			String str = iter.next();
			if("ccc".equals(str)) {
				all.remove("ccc"); //Collection集合方法
			}else{
                System.out.println(str);
            }
		}
	}
}
aaa
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1494)
	at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1517)
	at com.test.TestDemo.main(TestDemo.java:25)

这个时候报错为ConcurrentModificationException,也就是并发修改失败,此时无法进行删除操作,只能利用Iterator接口中的remove()删除处理:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Set<String> all = new HashSet<String>();
		all.add("aaa");
		all.add("bbb");
		all.add("ccc");
		all.add("ddd");
		Iterator<String> iter = all.iterator(); //实例化iterator对象
		while(iter.hasNext()) {
			String str = iter.next();
			if("ccc".equals(str)) {
				iter.remove(); //Collection集合方法
			}else {
				System.out.println(str);
			}
		}
	}
}
aaa
bbb
ddd

Collection中的remove()与Iterator中的remove()有什么不同?

在进行迭代输出的时候如果使用了Collection中的remove()就会造成并发更新异常,导致程序删除出错,此时只能使用Iterator中的remove()方法正常进行删除处理,但是正常情况下一样不建议使用删除操作。


ListIterator双向迭代输出

使用Iterator输出有一个特点,只允许由前向后输出,而如果需要双向迭代处理就要依靠Iterator的子接口:ListIterator,但需要注意的是,想要获取ListIterator接口对象Collection并没有定义相关的处理方法,但是List子接口有,也就是说这个接口专门是为List准备的。

在ListIterator接口中定义有如下的操作方法:

  • 判断是否有前一个元素:boolean hasPrevious();
  • 获取当前元素:E previous();
public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		all.add("aaa");
		all.add("bbb");
		all.add("ccc");
		all.add("ddd");
		ListIterator<String> iter = all.listIterator(); //实例化iterator对象
		System.out.println("由前向后");
		while(iter.hasNext()) {
			String str = iter.next();
			System.out.println(str);
		}
		System.out.println("由后向前");
		while(iter.hasPrevious()) {
			String str = iter.previous();
			System.out.println(str);
		}
	}
}
由前向后
aaa
bbb
ccc
ddd
由后向前
ddd
ccc
bbb
aaa

如果想实现由后向前的遍历,那么首先要实现由前向后的遍历处理,也就是说我们如果不进行hasNext()的操作的话,当前指针在第一个元素,而第一个元素的前一个元素是null,所以是不会进行由后向前的遍历的。

由于ListIterator只针对于List接口输出,所以在开发中使用的频率会小一些。


Enumeration输出

Enumeration是在JDK1.0的时候就提供的输出接口,这个接口主要是为了Vector类提供输出服务的,一直到后续的JDK发展,Enumeration依然只为Vector一个类服务,如果想获取Enumeration的对象就要依靠Vector类提供的方法:

  • 获取Enumeration:public Enumeration<E> elements();

在Enumeration的接口中定义有两个操作方法:

  • 是否有下一个元素:public boolean hasMoreElements();
  • 获取当前元素:public E nextElement();

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Vector<String> all = new Vector<String>();
		all.add("aaa");
		all.add("bbb");
		all.add("ccc");
		all.add("ddd");
		Enumeration<String> enu = all.elements();
		while(enu.hasMoreElements()) {
			String str = enu.nextElement();
			System.out.println(str);
		}
	}
}
aaa
bbb
ccc
ddd

由于该接口出现的时间比较长,所以在一些比较早的开发过程中,也有部分方法只支持Enumeration的输出,但随着类方法的不断完善,大部分的操作都利用的是Iterator实现了。


foreach输出

除了使用迭代接口实现输出之外,从JDK1.5开始加强型的for循环也可以实现集合的输出了,这种输出的形式与数组的输出操作形式类似:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		all.add("aaa");
		all.add("bbb");
		all.add("ccc");
		all.add("ddd");
		for(String str : all) {
			System.out.println(str);
		}
	}
}
aaa
bbb
ccc
ddd

这种形式的输出最初出现的时候并不建议使用,因为标准输出还是应该以Iterator为准,但毕竟JDK1.5都已经出了十几年了,很多语法也开始被大多数人所习惯。


Map集合

在之前已经学习了Collection接口以及对应的子接口,可以发之后现在Collection接口之中说保存的数据全部都只是单个对象,而在数据结构除了可以保存单个对象之外,实际上也可以进行二元偶对象的保存(key = value)的形式来存储,而存储二元偶对象的核心意义在于通过key获取对应的value。

在开发中Collection的集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。


Map接口简介

Map接口是进行二元偶对象保存的一个最大的父接口,该接口定义如下:

public interface Map<K, V>

该接口为一个独立的父接口,并且在实例化的时候要设置Key与Value的类型,也就是说在整体操作的时候要保存两个内容,在Map接口中定义有许多的操作方法,但需要记住以下几个核心方法:

  • 向集合之中保存数据:public V put(K key, V value);
  • 根据Key查询Value:public V get(Object key);
  • 将Map集合转为Set集合:public Set<Map.Entry<K, V>> entrySet();
  • 查询指定的Key是否存在:public boolean containsKey(Object key);
  • 将Map集合中的Key转为Set集合:public Set<K> keySet();
  • 根据Key删除Value:public default boolean remove(Object key, Object value);

其中put()、get()、entrySet()是需要掌握的核心方法,从JDK1.9之后Map接口中也扩充了一些静态方法供用户使用。

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = Map.of("one",1,"two",2);
		System.out.println(map);
	}
}
{two=2, one=1}
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = Map.of("one",1,"two",2,"one",3);
		System.out.println(map);
	}
}
Exception in thread "main" java.lang.IllegalArgumentException: duplicate key: one
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = Map.of("one",1,"two",2,null,3);
		System.out.println(map);
	}
}
Exception in thread "main" java.lang.NullPointerException

通过这三种代码,我们可以发现:在Map集合中,数据的保存就是按照“key = value”的形式存储的,并且使用of()方法时里面的数据是不允许重复的,否则会出现IllegalArgumentException的异常,如果设置内容为空则会报NullPointerException的异常。

对于现在见到的of()方法严格意义上来讲并不是Map集合的标准用法,正常开发时需要通过Map集合的子类来进行接口对象的实例化,而常用的子类:HashMap、Hashtable、TreeMap、LinkedHashMap。


HashMap子类 

HashMap是Map接口中最为常见的子类,该类的主要特点是无序存储,通过文档我们来观察一下HashMap子类的定义形式:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

该类的定义继承形式符合之前的集合定义形式,依然提供有抽象类,并且依然要重复实现Map接口:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);
		map.put("one", 111); //key重复
		map.put(null, 0); //key为空
		map.put("0", null); //value为空
		System.out.println(map.get("one"));
		System.out.println(map.get(null));
		System.out.println(map.get("123456"));
	}
}
111
0
null

这种操作形式是Map集合使用的最标准的处理形式,通过代码可以发现,通过HashMap实例化的Map接口可以针对于key或value保存null的数据,同时也了可以发现即便保存数据的key重复,也不会出现错误而是会出现内容的替换。

但对于Map接口中提供的put()方法本身是有返回值的,这个返回值指的是在重复key的情况下返回旧的value:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new HashMap<String, Integer>();
		System.out.println(map.put("one", 1));
		System.out.println(map.put("one", 111));
	}
}
null
1

可以看到在设置了相同的key时候put()方法会返回原始的数据内容。

清楚了HashMap的基本功能之后来研究一下HashMap的源代码,HashMap之中一定会存有大量的数据,对于数据的存储Map是如何进行的?来看一下HashMap的构造方法:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

在使用无参构造的时候会出现一个loadFactor属性,并且该属性默认的内容为0.75f:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

我们再来看一下put方法:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

在使用put()方法进行数据保存的时候会调用一个putVal()方法,同时会将key进行Hash处理,而在putVal()方法中依然会提供有一个Node()结点类进行数据的保存,而在使用putVal()方法操作的过程中会调用一个resize()的方法进行容量的扩充。

在进行HashMap的put()操作的时候是如何实现容量扩充的?

  • 在HashMap类中提供有一个常量作为初始化的容量配置,默认大小为16个元素:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • 也就是说默认可以保存的最大容量是16,当保存的内容容量超过了一个阈值(DEFAULT_LOAD_FACTOR = 0.75f;),也就是相当于“容量 * 阈值”(16 * 0.75 = 12)保存十二个元素的时候就会进行容量的扩充;
  • 在进行扩充的时候HashMap采用的是成倍的扩充模式,即每一次都扩充原来容量的两倍。

解释一下HashMap的工作原理?(JDK1.8之后)

  • 在HashMap之中进行数据存储的依然是利用了Node类完成的,在这种情况下可以使用的数据结构有:链表(时间复杂度“O(n)”)、二叉树(时间复杂度“O(log n)”);
  • 从JDK1.8开始,HashMap的实现出现了改变因为要适应大数据时代的海量数据问题,所以对于其存储发生了变化,并且在HashMap的内部提供有一个重要的常量:static final int TREEIFY_THRESHOLD = 8;
  • 在使用HashMap保存数据的时候,如果保存的个数没有超过阈值8(TREEIFY_THRESHOLD)就会按照链表的形式进行存储,而如果超过了这个阈值就会将链表转为红黑树以实现树的平衡,并利用左旋与右旋保证数据的查询性能。

LinkedHashMap子类

HashMap虽然是Map集合最为常用的一种子类,但是其本身所保存的数据都是无序的,虽然有序与否对Map没有影响,但现在我们希望Map集合中保存数据的顺序为其增加顺序,则就可以更换子类为LinkedHashMap,这个操作使基于链表实现的。

首先我们来看一下LinkedHashMap的定义:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

既然是链表存储,我们知道链表的存储长度是有限的,所以使用LinkedHashMap类的时候往往数据量不要特别大,因为会造成时间复杂度攀升,通过继承结构可以发现LinkedHashMap使HashMap的子类,继承关系如下:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new LinkedHashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);	
		map.put("one", 111); //key重复
		map.put(null, 0); //key为空
		map.put("0", null); //value为空
		System.out.println(map);
		
	}
}
{one=111, two=2, null=0, 0=null}

通过程序执行可以发现当使用LinkedHashMap进行存储之后,所有数据的保存顺序就是添加顺序。


Hashtable子类

Hashtable类是从JDK1.0的时候就提供的,与Vector和Enumeration属于最早的一批动态数组的实现类,后来为了将其继续保存下来,所以让其多实现了一个Map接口,Hashtable类的定义结构如下:

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new Hashtable<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);	
		map.put("one", 111); //key重复
		map.put(null, 0); //key为空
		map.put("0", null); //value为空
		System.out.println(map);
	}
}
Exception in thread "main" java.lang.NullPointerException

这时候就会发现不能存储null的数据,我们去掉之后再来看一下:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new Hashtable<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);	
		map.put("one", 111); //key重复
		System.out.println(map);
	}
}
{two=2, one=111}

通过观察可以发现Hashtable中进行数据存储的时候,设置的key或者value都不允许为空,否则会出现NullPointerException的异常。

解释一下HashMap与Hashtable的区别?

  • HashMap中的方法都属于异步操作,非线程安全,允许保存空的数据;
  • Hashtable中的方法都属于同步方法,线程安全,不允许保存空,否则会出现NullPointerException的异常;

Map.Entry内部接口

虽然已经清楚了整个Map集合的基本操作形式,但依然有一个核心的问题需要解决,Map集合中是如何进行数据存储的?

对于List而言(LinkedList子类)依靠的是链表的形式实现的数据存储,那么在进行数据存储的时候一定要将数据保存在Node结点中;虽然在HashMap之中也可以见到Node类型定义,通过源代码中的定义可以发现HashMap中的Node内部类本身实现了Map.Entry的接口:static class Node<K,V> implements Map.Entry<K,V>

所以可以得出的结论就是,所有的key和value的数据都被封装在Map.Entry接口中,而接口定义如下:

public static interface Entry<K, V>

并且在这个内部接口中提供有两个重要的操作方法:

  • 获取指定的key:public K getKey();
  • 获取value:public V getValue();

在JDK1.9之前的开发版本中,使用者基本都不会去考虑创建Map.Entry的对象,实际上在正常的开发过程中使用者也不需要关心Map.Entry对象的创建,可是从JDK1.9之后Map接口中追加有一个新的方法:

  • 创建Map.Entry对象:public static <K, V> Entry<K, V> entry(K k, V v);
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map.Entry<String, Integer> entry = Map.entry("one", 123);
		System.out.println("key:" + entry.getKey());
		System.out.println("value:" + entry.getValue());
		System.out.println(entry.getClass().getName());
	}
}
key:one
value:123
java.util.KeyValueHolder

这时我们会发现使用的是java.util.KeyValueHolder,我们打开代码看一下:

final class KeyValueHolder<K,V> implements Map.Entry<K,V>

通过分析可以发现,在整个Map集合中,Map.Entry的主要作用就是作为一个key和value的包装类型使用,而大部分情况在进行数据存储的时候都会将key和value包装为一个Map.Entry对象进行使用。


利用Iterator输出Map集合

对于集合输出而言,最标准的做法就是利用Iterator接口来完成,但是需要明确的一点是在Map的集合中并没有一个方法直接返回Iterator接口对象,所以这种情况下就必须分析不直接提供Iterator接口实例化操作的原因,我们使用Collection与Map集合的存储结构进行一个比较处理。

我们会发现在Map集合中保存的实际上是一组Map.Entry接口对象(包装的是key和value),所以整个Map依然实现的是单值保存,这样在Map集合中提供有一个方法:public Set<Map.Entry<K, V>> entrySet();,用于将所有的Map集合转换为Set集合。

经过分析我们发现如果想要使用Iterator实现Map集合的输出,则必须按照如下步骤处理:

  • 利用Map接口中的entrySet()方法将Map集合转为Set集合;
  • 利用Set接口中的iterator()将Set集合转为Iterator接口实例;
  • 利用Iterator进行迭代输出,获取每一组的Map.Entry对象,随后使用getKey()和getValue()获取数据;
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);
		Set<Map.Entry<String, Integer>> set = map.entrySet();
		Iterator<Map.Entry<String, Integer>> iter = set.iterator();
		while(iter.hasNext()) {
			Map.Entry<String, Integer> me = iter.next();
			System.out.println(me.getKey() + ":" + me.getValue());
		}
	}
}
one:1
two:2

虽然Map集合本身支持迭代输出,但如果从实际开发来讲,Map集合最主要的用法在于实现数据的key的查找,如果现在不使用iterator而使用foreach语法输出也需要将Map集合转为Set集合:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);
		Set<Map.Entry<String, Integer>> set = map.entrySet();
		for(Map.Entry<String, Integer> me : set) {
			System.out.println(me.getKey() + ":" + me.getValue());
		}
	}
}
one:1
two:2

由于Map迭代输出的情况相对较少,所以对于此类的语法应该深入理解并且一定要掌握。


自定义Map的key类型

在使用Map集合的时候可以发现对于key和value的类型都可以由使用者任意决定,那么也就意味着依然可以使用自定义的类来进行key的设置:

class Person{
	private String name;
	private int age;
	/**
	 * @param name
	 * @param age
	 */
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<Person, String> map = new HashMap<Person, String>();
		map.put(new Person("张三", 12), "001");
		map.put(new Person("李四", 22), "002");
		System.out.println(map.get(new Person("张三", 12)));;
	}
}

这时候我们使用get()方法来查找value的值:

null

这个问题的分析在之前分析HashMap的时候已经说过了,HashMap会将key做一个hash,而get()方法也是使用了一个hash()方法:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

也就是说存储的时候是有hash数据的,也就证明查询的过程中要利用hashCode()来查询,当我们使用getNode()方法查询的时候可以看到也使用了equals()方法,所以对于自定义key类型所在类中一定要覆写hashCode()与equals()方法:

class Person{
	private String name;
	private int age;
	/**
	 * @param name
	 * @param age
	 */
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Person other = (Person) obj;
		if (age != other.age)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Map<Person, String> map = new HashMap<Person, String>();
		map.put(new Person("张三", 12), "001");
		map.put(new Person("李四", 22), "002");
		System.out.println(map.get(new Person("张三", 12)));;
	}
}
001

虽然允许使用自定义类型的类作为key的类型,但也需要注意一点,在实际的开发中对于Map常用的类型就是String、Long、Integer,尽量使用系统类。

如果在进行HashMap进行数据操作的时候出现了Hash冲突(Hash码相同),HashMap是如何解决的?

当出现了Hash冲突的时候,为了保证数据的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表保存:


集合工具类


Stack栈操作

栈是一种先进后出的数据结构。例如:在文本编辑器上都有撤销功能,在每次使用的时候,最后一次的编辑操作姻缘石最先撤销,这个功能就是利用栈来实现的,栈的基本操作形式如下:

在Java中使用Stack来描述栈的操作,这个类的定义如下:

public class Stack<E> extends Vector<E>

可以发现Stack是Vector的子类,但是它使用的并不是Vector类之中所提供的方法,而是采用如下的两个方法:

  • 入栈:public E push(E item);
  • 出栈:public synchronized E pop();
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Stack<String> all = new Stack<String>();
		all.push("A");
		all.push("B");
		all.push("C");
		all.push("D");
		System.out.println(all.pop());
		System.out.println(all.pop());
		System.out.println(all.pop());
		System.out.println(all.pop());
		System.out.println(all.pop());
	}
}
D
C
B
A
Exception in thread "main" java.util.EmptyStackException

通过此时的操作可以发现,所有保存的数据会按照之前保存顺序的逆序输出,如果栈已经空了,就会报出EmptyStackException的异常。


Queue队列

Queue描述的是一个队列,而队列的主要特点是实现先进先出的操作形式,其基本的操作形式如下:

如果将队列应用在多线程的“生产者与消费者”的模型处理上,那么对生产者过快的情况下没有必要等待消费者获取数据了,可以直接将数据保存在队列中,队列的实现可以使用LinkedList子类来完成,这个类的定义为:

队列的使用主要靠Queue接口之中提供的方法来处理,提供有如下方法:

  • 像队列中追加数据:public boolean offer(E e); 也可以直接使用add()方法;
  • 通过队列获取数据:E poll(); 弹出后删除数据;
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Queue<String> queue = new LinkedList<String>();
		queue.offer("a");
		queue.offer("b");
		queue.offer("c");
		queue.offer("d");
		System.out.println(queue.poll());
		System.out.println(queue.poll());
		System.out.println(queue.poll());
		System.out.println(queue.poll());
		System.out.println(queue.poll());
	}
}
a
b
c
d
null

还有一个是优先级操作队列:public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable;

public class TestDemo {
	public static void main(String[] args) throws Exception {
		Queue<String> queue = new PriorityQueue<String>();
		queue.offer("b");
		queue.offer("c");
		queue.offer("a");
		queue.offer("d");
		System.out.println(queue.poll());
		System.out.println(queue.poll());
		System.out.println(queue.poll());
		System.out.println(queue.poll());
		System.out.println(queue.poll());
	}
}
a
b
c
d
null

可以发现优先级队列有排序,那就说明它和Comparable接口有关:

对于队列的选用原则需要根据项目环境来决定的,但是这两个队列的特征大家要了解。


Properties属性操作

在Java中有一种*.properties的文件,那么这类文件的存储结构是按照“key = value”的形式存储的,而这种结构的保存形式和Map集合很相似,唯一的区别是其所保存的内容只能是字符串,为了可以方便的描述属性的定义,java.util中提供有Properties类型,此类是HashTable的子类:

public class Properties extends Hashtable<Object,Object>

我们可以发现在继承HashTable的时候为HashTable中定义的泛型为Object,实际上Properties是不需要操作泛型的,它操作的类型只能是String类,在Properties中要想实现一个属性的操作,可以采用如下的方法:

  • 设置属性:public synchronized Object setProperty(String key, String value);
  • 取得属性,key不存在返回空:public String getProperty(String key);
  • 取得属性,不存在返回默认值:public String getProperty(String key, String defaultValue);
  • 输出属性内容:public void store(OutputStream out, String comments) throws IOException;
  • 通过输入流读取属性内容:public synchronized void load(Reader reader) throws IOException;
public class TestDemo {
	public static void main(String[] args) throws Exception {
		Properties prop = new Properties();
		prop.setProperty("csdn", "www.csdn.com");
		prop.setProperty("csdnzijun", "zijun");
		System.out.println(prop.getProperty("csdn"));
		System.out.println(prop.getProperty("csdnzijun"));
		System.out.println(prop.getProperty("baidu"));
		System.out.println(prop.getProperty("baidu", "NoFound"));
	}
}
www.csdn.com
zijun
null
NoFound

通过代码发现Properties里面可以想Map集合那样进行内容的设置与获取,当时唯一的差别是它只能够操作String类型,之所以会提供有Properties类还有一个最重要的功能是它可以根据输出流输出属性,也可以根据输入流读取属性内容,这个功能Map是没有的,而将输出的内容保存在文件中的时候,Properties就会有一个自动将中文进行转码处理。

Properties最大的特点是可以进行资源内容的输入与输出的处理操作,但在实际开发中Properties往往是用来读取配置资源的信息,这一点主要是在标准设计之中做结构初始化准备时使用。


Collections工具类

Collections是Java提供的一组集合数据的操作工具类,利用它可以实现各个集合的操作:

增加操作: 

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		Collections.addAll(all, "hello", "world", "zijun");
		System.out.println(all);
	}
}
[hello, world, zijun]

反转操作: 

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		Collections.addAll(all, "hello", "world", "zijun");
		Collections.reverse(all);
		System.out.println(all);
	}
}
[zijun, world, hello]

排序,二分法查找操作: 

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		Collections.addAll(all, "hello", "world", "zijun");
		Collections.sort(all);
		System.out.println(Collections.binarySearch(all, "world"));
	}
}
1

大部分情况下对于集合的使用没有这么复杂的情况,更多的情况要么进行输出要么进行查询。

解释Collection和Collections的区别?

  • Collection是集合接口,允许保存单值对象;
  • Collections是集合操作的工具类,两者没有本质联系。

Stream数据流

从JDK1.8开始,由于已经进入了大数据时代,所以在类集中也支持有数据的流式分析处理操作,为此就专门提供了Stream的接口,同时在Collection接口中也提供有为此接口实例化的方法:

  • 获得Stream接口对象:public default Stream<E> stream();

Stream基本操作

Stream主要功能为进行数据的分析处理,同时主要是针对于集合中的数据进行分析操作:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		Collections.addAll(all, "hello", "world", "zijun");
		Stream<String> stream = all.stream();
        //将每一个元素变为小写,然后判断字母j是否存在,并打印含有j的元素个数
		System.out.println(stream.filter((ele)->ele.toLowerCase().contains("j")).count());
	}
}
1

现在我们就实现了一个基础的Stream操作,而更多的情况下我们可能要获取的是里面满足条件的数据内容,所以此时可以实现数据采集功能:

public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		Collections.addAll(all, "hello", "world", "zijun");
		Stream<String> stream = all.stream();
		List<String> result = stream.filter((ele)->ele.toLowerCase().contains("j")).collect(Collectors.toList());
		System.out.println(result);
	}
}
[zijun]

在Stream数据流处理的过程中还允许进行数据的分页处理,提供有两个方法:

  • 设置操作的最大长度:public Stream<T> limit(long maxSize);
  • 跳过指定数据量:public Stream<T> skip(long n);
public class TestDemo {
	public static void main(String[] args) throws Exception {
		List<String> all = new ArrayList<String>();
		Collections.addAll(all, "java", "jsp", "json", "javascript", "junit","hello", "world", "zijun");
		Stream<String> stream = all.stream();
		List<String> result = stream.filter((ele)->ele.toLowerCase().contains("j")).skip(2).limit(2).collect(Collectors.toList());
		System.out.println(result);
	}
}
[json, javascript]

Stream主要是利用自身的特带你进行数据的分析处理操作。


MapReduce基础模型

在进行数据分析的处理中有一个最基础的数据处理模型:MapReduce模型,对于这个模型一共有两个部分:Map处理部分、Reduce的分析部分,在进行数据的分析之前必须要对数据进行合理的处理,然后才可以做统计分析操作。

class Order{ //订单信息
	private String name;
	private double price;
	private int amout;
	/**
	 * @param name
	 * @param price
	 * @param amout
	 */
	public Order(String name, double price, int amout) {
		super();
		this.name = name;
		this.price = price;
		this.amout = amout;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public double getPrice() {
		return price;
	}
	public void setPrice(double price) {
		this.price = price;
	}
	public int getAmout() {
		return amout;
	}
	public void setAmout(int amout) {
		this.amout = amout;
	}
	
}

public class TestDemo {
	public static void main(String[] args) throws Exception {
	//如果要想使用Stream进行分析处理,就要将全部要分析的数据保存在集合中
		List<Order> all = new ArrayList<Order>();
		all.add(new Order("A小汽车", 10.0, 50));
		all.add(new Order("A洋娃娃", 50.0, 10));
		all.add(new Order("A乐高", 1050.9, 88));
		all.add(new Order("Steam打折游戏", 39.9, 100));
		all.add(new Order("A笔记本", 10998.8, 5));
		all.add(new Order("B小汽车", 10.0, 50));
		all.add(new Order("B洋娃娃", 50.0, 10));
		all.add(new Order("B乐高", 1050.9, 88));
		all.add(new Order("Steam打折游戏", 39.9, 100));
		all.add(new Order("B笔记本", 10998.8, 5));
		all.add(new Order("C小汽车", 10.0, 50));
		all.add(new Order("C洋娃娃", 50.0, 10));
		all.add(new Order("C乐高", 1050.9, 88));
		all.add(new Order("Steam打折游戏", 39.9, 100));
		all.add(new Order("C笔记本", 10998.8, 5));
		DoubleSummaryStatistics stat = all.stream().filter((ele)->ele.getName().contains("A"))
				.mapToDouble((orderObject)->orderObject.getPrice() * orderObject.getAmout()).summaryStatistics();
		System.out.println("购买数量" + stat.getCount());
		System.out.println("购买总价" + stat.getSum());
		System.out.println("平均花费" + stat.getAverage());
		System.out.println("最高花费" + stat.getMax());
		System.out.println("最高花费" + stat.getMin());
	}
}
购买数量4
购买总价148473.2
平均花费37118.3
最高花费92479.20000000001
最高花费500.0

这些分析操作只是JDK本身提供的支持,而实际中肯定不可能这样进行分析操作,因为所有的数据都保存在内存中的话,面对大数据环境绝对会崩溃。


类集的知识点终于处理完了,Java系列也要告一段落了,由于疫情一直没有开学,在家里学到的东西比我在学校里三年学到的还多,接下来我会继续学习JavaWeb的知识,期待下一次更新,我们下次见👋

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值