Java集合操作

2 篇文章 0 订阅

一.Collection

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:

  • List:一种有序列表的集合,例如,按索引排列的StudentList
  • Set:一种保证没有重复元素的集合,例如,所有无重复名称的StudentSet
  • Map:一种通过键值(key-value)查找的映射表集合,例如,根据Studentname查找对应StudentMap

Java集合的设计有几个特点:一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayListLinkedList等,二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:

List<String> list = new ArrayList<>(); // 只能放入String类型

最后,Java访问集合总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。

由于Java的集合设计非常久远,中间经历过大规模改进,我们要注意到有一小部分集合类是遗留类,不应该继续使用:

  • Hashtable:一种线程安全的Map实现;
  • Vector:一种线程安全的List实现;
  • Stack:基于Vector实现的LIFO的栈。

还有一小部分接口是遗留接口,也不应该继续使用:

  • Enumeration<E>:已被Iterator<E>取代。

Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括ListSetMap。Java集合使用统一的Iterator遍历,尽量不要使用遗留接口。

二.List

1.创建List

List<String> list = new ArrayList<>();

除了使用ArrayListLinkedList,我们还可以通过List接口提供的of()方法,根据给定元素快速创建List

List<Integer> list = List.of(1, 2, 5);

但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。

2.遍历List

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("apple", "pear", "banana");
        for (String s : list) {
            System.out.println(s);
        }
    }
}
import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 构造从start到end的序列:
        final int start = 10;
        final int end = 20;
        List<Integer> list = new ArrayList<>();
        for (int i = start; i <= end; i++) {
            list.add(i);
        }
        // 洗牌算法shuffle可以随机交换List中的元素位置:
        Collections.shuffle(list);
        // 随机删除List中的一个元素:
        int removed = list.remove((int) (Math.random() * list.size()));
        int found = findMissingNumber(start, end, list);
        System.out.println(list.toString());
        System.out.println("missing number: " + found);
        System.out.println(removed == found ? "测试成功" : "测试失败");
    }

    static int findMissingNumber(int start, int end, List<Integer> list) {
        final int start1 = 10;
        final int end1 = 20;
        List<Integer> list1 = new ArrayList<>();
        for (int i = start1; i <= end1; i++) {
            list1.add(i);
        }
        for(Integer s : list1){
            if (!list.contains(s.intValue())) {
                return s.intValue();
            }
        }
        return 0;
    }
}

 

三.equals

List内部并不是通过==判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等。

要正确使用Listcontains()indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。我们之所以能正常放入StringInteger这些对象,是因为Java标准库定义的这些类已经正确实现了equals()方法。

public class Main {
    public static void main(String[] args) {
        List<Person> list = List.of(
            new Person("Xiao Ming"),
            new Person("Xiao Hong"),
            new Person("Bob")
        );
        System.out.println(list.contains(new Person("Bob"))); // false
    }
}

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}


//虽然放入了new Person("Bob"),但是用另一个new Person("Bob")查询不到,原因就是Person类没有覆写equals()方法。

编写equals

equals()方法的正确编写方法:

  1. 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
  2. instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false
  3. 对引用类型用Objects.equals()比较,对基本类型直接用==比较。

使用Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。

如果不调用Listcontains()indexOf()这些方法,那么放入的元素就不需要实现equals()方法。

public boolean equals(Object o) {
    if (o instanceof Person) {
        Person p = (Person) o;
        return Objects.equals(this.name, p.name) && this.age == p.age;
    }
    return false;
}
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
//        List<Person> list = List.of(
//                new Person("Xiao", "Ming", 18),
//                new Person("Xiao", "Hong", 25),
//                new Person("Bob", "Smith", 20)
//        );
        List<Person> list = new ArrayList<>();
        list.add(new Person("Xiao", "Ming", 18));
        list.add(new Person("Xiao", "Hong", 25));
        list.add(new Person("Bob", "Smith", 20));

        boolean exist = list.contains(new Person("Bob", "Smith", 20));
        System.out.println(exist ? "测试成功!" : "测试失败!");
    }
}

class Person {
    String firstName;
    String lastName;
    int age;
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public boolean equals(Object o) {
        if (o instanceof Person) {
            Person p = (Person) o;
            return Objects.equals(this.firstName, p.firstName) &&Objects.equals(this.lastName, p.lastName)&& this.age == p.age;
        }
        return false;
    }

}

List中查找元素时,List的实现类通过元素的equals()方法比较两个元素是否相等,因此,放入的元素必须正确覆写equals()方法,Java标准库提供的StringInteger等已经覆写了equals()方法;

编写equals()方法可借助Objects.equals()判断。

如果不在List中查找元素,就不必覆写equals()方法。

四.Map

Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把keyvalue做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是HashMap

如果只是想查询某个key是否存在,可以调用boolean containsKey(K key)方法。

始终牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。

遍历Map

Map来说,要遍历key可以使用for each循环遍历Map实例的keySet()方法返回的Set集合,它包含不重复的key的集合:

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 123);
        map.put("pear", 456);
        map.put("banana", 789);
        for (String key : map.keySet()) {
            Integer value = map.get(key);
            System.out.println(key + " = " + value);
        }
    }
}
class Students {
	List<Student> list;//定义了一个Student的List,名为list,可能十分巨大
	Map<String, Integer> cache;//定义了一个Map,key为String类型,value为Integer类型
	//作为缓存来存放那些被查询频率很高的学生,比如成绩差爱惹事的学生,下次查询直接从这里面找就会很快

	Students(List<Student> list) {
		this.list = list;
		cache = new HashMap<>();
		
	}

	int getScore(String name) {
		// a.有个娃犯事了,先看看重点关注对象(cache)里有没有这娃
		Integer score = this.cache.get(name);//试图从cache中查找这娃对应的成绩
		
		if (score == null) {//b.如果成绩为null,说明没有重点关注他(cache里没有他的名字)
			// TODO:
			score = findInList(name);//c.我们再在学校的超级学生名册里来查找一下
			if (score!= null) {//d.如果有成绩记录,score不为null,看来确实是我们学校的学生
				cache.put(name, score);//e.立即把他放到重点关注对象里来,下次犯事了很快就能查到他所有信息 
			}
		}
		return score == null ? -1 : score.intValue();//f.这里不太好理解,为什么会再次判断score是否为null,我们来分析一下:
		//这里有两种情况:
		//1.当在执行步骤b的判断时如果score!=null,说明重点关注对象里有他,直接跳过if循环,return时score==null为假,返回他的成绩
		//2.当在执行步骤b的判断时如果score==null,if循环里的程序会被执行,这里步骤c的语句 score = findInList(name)会重新对score进行赋值
		//根据下面findInList(String name)方法我们知道它可能返回null,也可能返回分数,所以最后return的时候会再次判断score是否为null
		//如果score为null则返回-1,表示这个学生不在超级学生名单里,不是学校里的娃
		
	}

	Integer findInList(String name) {
		for (var ss : this.list) {
			if (ss.name.equals(name)) {
				return ss.score;
			}
		}
		return null;//表示查询的人不在名单内
	}
}

Map是一种映射表,可以通过key快速查找value

可以通过for each遍历keySet(),也可以通过for each遍历entrySet(),直接获取key-value

最常用的一种Map实现是HashMap

五.equals和hashCode

Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。

我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。

正确使用Map必须保证:

  1. 作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true

  2. 作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:

  • 如果两个对象相等,则两个对象的hashCode()必须相等;
  • 如果两个对象不相等,则两个对象的hashCode()尽量不要相等。
public class Person {
    String firstName;
    String lastName;
    int age;


    @Override
    int hashCode() {
        int h = 0;
        h = 31 * h + firstName.hashCode();
        h = 31 * h + lastName.hashCode();
        h = 31 * h + age;
        return h;
    }

    //编写equals()和hashCode()遵循的原则是:equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。
    int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }

}



//引用类型使用Objects.equals()比较,基本类型使用==比较。
package test.MapDemo;
import java.util.*;

public class equalsDemo2 {
    public static void main(String[] args) {
        Person p1 = new Person("zhang",12);
        Map<Person,Integer> map = new HashMap<>();
        map.put(p1,123);
        Person p2 = new Person("zhang",12);
        System.out.println(p1==p2);
        System.out.println(p1.equals(p2));
        System.out.println(map.get(p1));
        System.out.println(map.get(p2));
    }
}

class Person{
    public String name;
    public int age;
    public Person(){}
    public Person(String n,int a){
        name = n;
        age = a;
    }

    @Override    public boolean equals(Object obj) {
        if(obj instanceof Person){
            Person p = (Person) obj;
            return Objects.equals(this.name,p.name) && this.age == p.age;
        }
        return false;
    }

    public int hashCode(){
        return Objects.hash(name,age);
    }
}


false
true
123
123


p1和p2都是Person类的对象,虽然地址值不一样,但是内容相同

所以map.get()获取的value值是相同的

要正确使用HashMap,作为key的类必须正确覆写equals()hashCode()方法;

一个类如果覆写了equals(),就必须覆写hashCode(),并且覆写规则是:

  • 如果equals()返回true,则hashCode()返回值必须相等;

  • 如果equals()返回false,则hashCode()返回值尽量不要相等。

实现hashCode()方法可以通过Objects.hashCode()辅助方法实现

六.set

Map用于存储key-value的映射,对于充当key的对象,是不能重复的,并且,不但需要正确覆写equals()方法,还要正确覆写hashCode()方法。

如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set

Set用于存储不重复的元素集合,它主要提供以下几个方法:

  • 将元素添加进Set<E>boolean add(E e)
  • 将元素从Set<E>删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)

Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。

因为放入Set的元素和Map的key类似,都要正确实现equals()hashCode()方法,否则该元素无法正确地放入Set

Set接口并不保证有序,而SortedSet接口则保证元素是有序的:

  • HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
  • TreeSet是有序的,因为它实现了SortedSet接口。

使用TreeSet和使用TreeMap的要求一样,添加的元素必须正确实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。

在聊天软件中,发送方发送消息时,遇到网络超时后就会自动重发,因此,接收方可能会收到重复的消息,在显示给用户看的时候,需要首先去重。请练习使用Set去除重复的消息:

public class Main {
    public static void main(String[] args) {
//        List<Message> received = List.of(
//                new Message(1, "Hello!"),
//                new Message(2, "发工资了吗?"),
//                new Message(2, "发工资了吗?"),
//                new Message(3, "去哪吃饭?"),
//                new Message(3, "去哪吃饭?"),
//                new Message(4, "Bye")
//        );
        List<Message> received = new ArrayList<>();
        received.add(new Message(1, "Hello!"));
        received.add(new Message(2, "发工资了吗?"));
        received.add(new Message(2, "发工资了吗?"));
        received.add(new Message(3, "去哪吃饭?"));
        received.add(new Message(3, "去哪吃饭?"));
        received.add(new Message(4, "Bye"));
        List<Message> displayMessages = process(received);
        for (Message message : displayMessages) {
            System.out.println(message.text);
        }
    }

    //方法一:用到sequence来判断,而做这个判断最好的就是用Set来判断要加到List中的Message对象的sequence字段是不是已经存在了
    static List<Message> process(List<Message> received) {
        // TODO: 按sequence去除重复消息
        Set<Integer> map = new HashSet<>();
        List<Message> receiveds = new ArrayList<>();
        for(Message message: received){
            if(!map.contains(message.sequence)){
                map.add(message.sequence);
                receiveds.add(message);
            }
        }
        return receiveds;
    }

    //方法二:重写message对象的equals和hashMap使message类可以满足放入set,然后用以message泛型的set就可以自动去重。
    //用list和set的相互转换
    static List<Message> process(List<Message> received) {
        HashSet<Message> messageSet = new HashSet<>(received);
        ArrayList<Message> messageList = new ArrayList<>(messageSet);
        return messageList;
    }

    //写在message类中
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Message message = (Message) o;
        return sequence == message.sequence && Objects.equals(text, message.text);
    }
    @Override
    public int hashCode() {
        return Objects.hash(sequence, text);
    }
}

class Message {
    public final int sequence;
    public final String text;
    public Message(int sequence, String text) {
        this.sequence = sequence;
        this.text = text;
    }
}

七.Queue

Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表.

 注意:不要把null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。(调用poll()方法来取出队首元素,当获取失败时,它不会抛异常,而是返回null)

队列Queue实现了一个先进先出(FIFO)的数据结构:

  • 通过add()/offer()方法将元素添加到队尾;
  • 通过remove()/poll()从队首获取元素并删除;
  • 通过element()/peek()从队首获取元素但不删除。

要避免把null添加到队列。

参考:集合 - 廖雪峰的官方网站

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值