浅谈java中的容器2

252 阅读11分钟

今天说一下Set接口下的容器和Map容器,说的不好还请指正,感谢.


1.Set接口

Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素.

Set 接口存储一组唯一,无序的对象,且最多包含一个null元素.

下面介绍Set的两个实现类

1.1 HashSet不一定一致

该类实现了Set接口,存储的数据不允许出现重复元素,且不能保证集合中元素的顺序,即向集合中存储数据的顺序跟在集合中存储数据的顺序 不一定一样.

HashSet的底层是由哈希表(HashTable)实现的

HashSet的特点是查询、增删效率高,但是无序且不可重复

推荐使用HashSet

示例1:

        import java.util.HashSet;
        import java.util.Set;

        public class TestHashSet {
        	public static void main(String[] args) {
            	Set<Person>set=new HashSet<Person>();
            	set.add(new Person("张三",12));
            	set.add(new Person("李四",18));
            	set.add(new Person("王五",16));
            	set.add(new Person("赵六",21));
            	set.add(new Person("赵六",21));
            	System.out.println(set);
        	}
        }
        class Person {
        	private String name;
        	private int age;
        	public Person(String name, int age) {
        		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 + "]";
        	}
        }

打印结果为:

    [Person [name=王五, age=16], Person [name=赵六, age=21], Person [name=张三, age=12], Person [name=李四, age=18], Person [name=赵六, age=21]]

我们注意到有两个Person [name=赵六, age=21],这是怎么回事呢?

我们在HashSet中放入数据时,先是根据哈希算法hashcode()方法计算这个数据应该存入哈希数组中的那个位置;确定了数据存入的位置后,再判断这个数据在这个位置上是否重复,调用equals()方法进行判断.如果返回true就表示这个数据重复,不添加;返回false就往里面添加数据.在同一个位置的数据是以链表的形式排列的,当一个位置的数据大于八个时,链表转为红黑树.

这样我们就明白了了,HashSet是根据hashCode()和equals()两个方法确定是否往集合中添加数据的.所以我们对这种两个Person[name=赵六,age=21]的情况只需要重写这两个方法就可以了.

下面是更改之后的Person类:

 class Person {
private String name;
private int age;
public Person(String name, int age) {
	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 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=李四, age=18], Person [name=王五, age=16], Person [name=张三, age=12], Person [name=赵六, age=21]]

所以当HashSet中存放的是自定义引用数据类型的变量时.当出现往HashSet中添加数据时会出现重复的情况时,只需重写hashCode()和equals()两个方法即可.

  • 如果两个对象的hashCode()值相同,不一定是一个对象,需要进一步比较equals()
  • 如果两个对象的hashCode()值不相同,肯定不是一个对象

1.2TreeSet

该类实现了Set接口,可以实现排序等功能,所以就不能使用多态.

TreeSet的底层是由TreeMap实现的,存储形式是红黑树

TreeSet的优点是其中的元素默认升序

应用场景可以是在需要去重,数据有排序且没有索引的的情况

示例2:

        import java.util.TreeSet;

        public class TestTree1 {
        	public static void main(String[] args) {
            	TreeSet<Integer> set = new TreeSet<Integer>();
            	set.add(12);
            	set.add(43);
        		set.add(23);
        		set.add(2);
            	set.add(1);
            	System.out.println(set);

        	}
        }

打印结果为:

[1, 2, 12, 23, 43]

我们可以看到存入的数据是默认升序的.

1.3比较器

来先看一下下面的程序: 示例3:

    import java.util.TreeSet;

    public class TestTreeSet {
    	public static void main(String[] args) {
        	TreeSet<Person>set=new TreeSet<Person>();
            	set.add(new Person("张三",12));
        	set.add(new Person("李四",18));
        	set.add(new Person("王五",16));
        	set.add(new Person("赵六",21));
        	System.out.println(set);
    	}
    }
    class Person{
    	private String name;
    	private int age;
    	public Person(String name, int age) {
        	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 + "]";
    	}
    }

运行之后的情况:

Exception in thread "main" java.lang.ClassCastException: com.shy.test.Person cannot be cast to java.lang.Comparable

有个异常!!!!

我们可以看到最后有个Comparable,这是一个接口.此接口强行对实现它的每个类的对象进行整体排序.

    int compareTo(T o)   比较此对象与指定对象的顺序 

因为TreeSet是默认对存入其中的数据进行升序,但是我们这个对象又没有实现这个这个接口,所以抛出了这个异常.

但是为什么我们向TreeSet中存入数字没有抛出这个异常呢,这是因为我们在向容器中存入基本数据类型的数据时,程序会自动把这个数据包装成对应的包装类.就像int被包装成了Integer.Integer实现了Comparable接口.

所以当我们向TreeSet中存入自定义引用数据类型的数据时,假若没有实现Comparable接口就会抛出异常.

下面是实现了Comparable接口的Person类:

class Person implements Comparable<Person>{
    private String name;
	private int age;
	public Person(String name, int age) {
    	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 o) {
    	return this.age-o.age;
	}
}

可以看到我们实现了Comparable接口,重写了其中的comparaTo()方法,这个时候就可以就可以根据age属性进行升序排列.

而且,实现了这个接口之后,还可以实现去重.

示例4:

    import java.util.TreeSet;

    public class TestTreeSet {
        public static void main(String[] args) {
	       	TreeSet<Person>set=new TreeSet<Person>();
    		set.add(new Person("张三",12));
    		set.add(new Person("李四",18));
    		set.add(new Person("王五",16));
    		set.add(new Person("赵六",21));
    		set.add(new Person("赵六",21));
    		System.out.println(set);
    	}
    }
    class Person implements Comparable<Person>{
	    private String name;
    	private int age;
    	public Person(String name, int age) {
	    	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 o) {
	    	return this.age-o.age;
    	}
    }

程序运行结果为:

    [Person [name=张三, age=12], Person [name=王五, age=16], Person [name=李四, age=18], Person [name=赵六, age=21]]

可以看到实现了升序以及去重的效果.

上面说的是内部比较器,实现Comparable接口,重写comparaTo() 方法.

但这种情况每次修改需要修改源码,不符合设计原则开闭原则:对修改关闭,对扩展开放.

所以下面介绍外部比较器:

使用外部比较器是要实现接口java.util.Comparator的接口,重写compare()方法,可以在方法中自定义比较规则,下面是这个接口中的方法

 int compare(T o1, T o2) 
      比较用来排序的两个参数

看一下外部比较器的使用:

示例5:

        import java.util.Comparator;
        import java.util.TreeSet;

        public class TestTreeSet {
        	public static void main(String[] args) {
        	    //匿名内部类
        		TreeSet<Person> set = new TreeSet<Person>(new Comparator<Person>() {
        			@Override
        			public int compare(Person o1, Person o2) {
        				return o1.getAge() - o2.getAge();
        			}
        		});
        		set.add(new Person("张三", 12));
        		set.add(new Person("李四", 18));
        		set.add(new Person("王五", 16));
        		set.add(new Person("赵六", 21));
        		set.add(new Person("赵六", 21));
        		System.out.println(set);
        	}
        }
        class Person {
        	private String name;
        	private int age;
        	public Person(String name, int age) {
        		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 + "]";
        	}
        }

程序运行结果为:

[Person [name=张三, age=12], Person [name=王五, age=16], Person [name=李四, age=18], Person [name=赵六, age=21]]

可以看到实现了按照age升序排列,且去重了.

因为我们实现这个接口就是为了重写其中的方法,所以为了简化代码,使用了匿名内部类.

所以如果我们在TreeSet中存储自定义引用数据类型的数据时,一定要设置比较器,内部比较器、外部比较器都行.但建议使用外部比较器.

2.Map接口

Map 接口存储一组键值对象,提供key(键)到value(值)的映射.

  • key的特点: 无序的,不可重复的 -->所有的key,就是一个set集合
  • value的特点: 无序的,可重复 --> Collection的特点
  • 一个key对象一个value值,如果想要对应多个,多个value值可以存在在一个数组|容器中
  • key相同时候,value会覆盖

2.1HashMap

  • HashMap的底层是哈希表的结构;
  • 该类实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步
  • HashMap中没有新增功能,可以使用多态
  • 因为所有的key值就是一个set集合,所以如果HashMap的key是自定义的引用数据类型,就需要对key的数据的类型重写hashCode()和equals()方法,实现去重,这个时候value会覆盖
  • 如果想要根据value值进行去重,我们可以手动使用containsValue()进行判断.

下面看一个HashMap的使用:

示例6:

    import java.util.HashMap;
    import java.util.Map;

    public class TestHashMap {
    	public static void main(String[] args) {
    		Map<Person, Integer> map = new HashMap<Person, Integer>();
    		map.put(new Person("张三", 12), 23);
    		map.put(new Person("李四", 18), 13);
    		map.put(new Person("王五", 16), 45);
    		map.put(new Person("赵六", 21), 56);
    		map.put(new Person("赵六", 21), 56);
    		System.out.println(map);
    	}
    }
    class Person {
    	private String name;
    	private int age;
    	public Person(String name, int age) {
    		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 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;
    	}
    }

可以看到,因为存储的自定义引用类型数据,所以重写了hashCode()和equals()方法,实现了去重.

2.2HashMap的遍历----->遍历key值

import java.util.*;

public class Test {
public static void main(String[] args) {
	Map<String, String> map = new HashMap<String, String>();
	map.put("A", "1");
	map.put("B", "2");
	map.put("C", "3");
	/*
	 * 第一种: Set<K> 
	 * 	    keySet() 返回此映射中所包含的键的 Set 视图
	 */
	System.out.println("通过Map.keySet遍历key和value:");
	for (String key : map.keySet()) {
		System.out.println("key= " + key + " and value= " + map.get(key));
	}
	/*
	 * 第二种: 迭代器 Iterator<E> 
	 * 			iterator() 返回在此 set 中的元素上进行迭代的迭代器
	 */
	System.out.println("通过Map.keySet使用iterator遍历key和value:");
	Set<String> set = map.keySet();
	Iterator<String> it = set.iterator();
	while (it.hasNext()) {
		String key = it.next();
		System.out.println("key= " + key + " and value= " + map.get(key));
	}
	/*
	 * 第三种:推荐,尤其容量大时 Set<Map.Entry<K,V>> 
	 * 			entrySet() 返回此映射所包含的映射关系的 Set 视图
	 */
	System.out.println("通过Map.entrySet遍历key和value");
	for (Map.Entry<String, String> entry : map.entrySet()) {
		System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
	}
}

}

2.3TreeMap

基于红黑树的 NavigableMap实现.该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法

因为TreeMap同样基于红黑树,所以我们在向其中存储自定义引用类型数据时,要写比较器.

下面看一下TreeMap的使用:

示例7:

    import java.util.Comparator;

import java.util.Map; import java.util.TreeMap;

    public class TestTreeMap {
        public static void main(String[] args) {
    		Map<Person, Integer> map = new TreeMap<Person, Integer>(new Comparator<Person>() {
    			@Override
    			public int compare(Person o1, Person o2) {
    				return o1.getAge() - o2.getAge();
    			}
    		});
    		map.put(new Person("张三", 12), 23);
    		map.put(new Person("李四", 18), 13);
    		map.put(new Person("王五", 16), 45);
    		map.put(new Person("赵六", 21), 56);
    		map.put(new Person("赵六", 21), 56);
    		System.out.println(map);
    	}
    }
    class Person {
    	private String name;
    	private int age;
    	public Person(String name, int age) {
    		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 + "]";
    	}
    }

程序运行结果为:

{Person [name=张三, age=12]=23, Person [name=王五, age=16]=45, Person [name=李四, age=18]=13, Person [name=赵六, age=21]=56}

实现了去重以及升序.

3.总结

昨天到今天说了六种容器,那么哪种容器适用于哪种情况呢,下面来说一下:

  • 如果需要查找以及获取元素的速度快,我们可以使用ArrayList
  • 如果需要大量的进行增删操作,我们可以使用LinkedList
  • 如果想要容器中的每个数据唯一且无序,我们可以使用HashSet
  • 如多想要容器中的每个数据唯一但是要升序或降序,我们可以使用TreeSet
  • 如果想要存储键值对数据且键值对无序唯一,我们就可以使用HashMap
  • 如果想要存储键值对数据且想让键值对根据其中的元素升序或降序,我们就可以使用TreeMap
  • 使用HashMap时,想要线程安全,我们可以使用 HashTable
  • 使用ArrayList时,想要线程安全,我们可以使用Vector