集合框架和泛型
本章技能目标
- 掌握集合框架包含的内容
- 掌握ArrayList和LinkedList的使用
- 掌握HashMap的使用
- 掌握Iterator的使用
- 掌握泛型集合的使用
1. 集合框架概述
Java集合框架为我们提供了一套性能优良、使用方便的接口和类,它们都位于java.util包中。Java集合框架包含的主要内容以及彼此之间的关系如图所示:
从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
- **接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象
- **实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
- **算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。
集合框架体系如图所示:
Java 集合框架提供了一套性能优良,使用方便的接口和类,java集合框架位于java.util包中, 所以当使用集合框架的时候需要进行导包。
集合框架定义了一些接口。下图提供了每个接口的概述:
序号 | 接口描述 |
---|---|
1 | Collection 接口 Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直接继承自Collection的类,只提供继承于的子接口(如List和set)。Collection 接口存储一组不唯一,无序的对象。 |
2 | List 接口 List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元素。List 接口存储一组不唯一,有序(插入顺序)的对象。 |
3 | Set Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。Set 接口存储一组唯一,无序的对象。 |
4 | Map Map 接口存储一组键值对象,提供key(键)到value(值)的映射。 |
8 | Iterator 接口 是负责定义访问和遍历元素的接口。 |
在集合框架中,List可以理解为前面讲过的数组,元素的内容可以重复并且有序,如下图所示:
Set可以理解为数学中的集合,里面数据不重复且无序,如下图所示:
Map也可以理解为数学中的集合,只是其中每个元素都由key和value两个对象组成,提供key到value的映射,如图所示:
2. List接口
2.1 List接口简介
-
List接口
继承了Collention接口(继承Iterable)
, -
可以允许重复的对象,可以插入多个null元素,输出的顺序就是插入的顺序
-
只能保存对象类型的数据,基本类型的数据会自动装箱成包装类
-
实现类有
ArrayList
、LinkedList
、Vactor
、Stack
,ArrayList 最为流行,它对数组进行了封装,实现了长度可变的数组,存储数据的方式与数组相同,都是在内存中分配连续的空间。可以使用索引的随意访问。它的优点在于遍历元素和访问元素的效率比较高。如图所示:而 LinkedList 采用链表存储方式。插入、删除元素时效率比较高。它提供了额外的addFirst()、addLast()、removeFirst()和removeLast()等方法,可以在LinkedList的首部或尾部进行插入或删除操作。这些方法使得LinkedList可被用作堆栈(statck)或者队列(queue)。如图所示:
2.2 ArrayList类
底层是动态数组结构,数据查询方便、数据增删改不方便,线程不安全,本质上就是通过创建新的更大的数组,将旧数组内容拷贝到新数组,来实现扩容
。
当调用
无参构造方法
来创建ArrayList时,默认容量为10
, 当添加的数据容量超过
数组容量大小时,会产生一个新数组
,新数组容量大小为原数组容量的1.5倍
,接着把原数组中的数据复制到新数组中。
常用的方法如下表:
方法名 | 说 明 |
---|---|
boolean add(Object o) | 在列表的末尾顺序添加元素, 起始索引位置从0开始 |
void add(int index,Object o) | 在指定的索引位置添加元素。 索引位置必须介于0和列表中元素个数之间 |
int size() | 返回列表中的元素个数 |
Object get(int index) | 返回指定索引位置处的元素。取出的元素是Object类型,使用前需要进行强制类型转换 |
boolean contains(Object o) | 判断列表中是否存在指定元素 |
boolean remove(Object o) | 从列表中删除元素 |
Object remove(int index) | 从列表中删除指定位置元素, 起始索引位置从0开始 |
2.3 LinkedList集合类
LinkedList底层数据结构是环形双向链表
,查询慢,增删快,线程不安全
(LinkedList实现了接口Deque,可以被当做队列或者栈使用)
它内部封装的是双向链表的数据结构,每个节点是一个Node对象
,Node对象中封装的是你要添加的元素,还有一个指向上一个Node对象的引用和一个指向下一个Node对象的引用。
每个节点都应该有3部分内容:
private static class Node<E> {
Node<E> prev;//前一个节点
E item;//本节点保存的数据
Node<E> next;//后一个节点
//省略其他内容。。。。
}
LinkedList除了包含ArrayList表中的方法外,还包含一些特殊的方法
方法名 | 说 明 |
---|---|
void addFirst(Object o) | 在列表的首部添加元素 |
void addLast(Object o) | 在列表的末尾添加元素 |
Object getFirst() | 返回列表中的第一个元素 |
Object getLast() | 返回列表中的最后一个元素 |
Object removeFirst() | 删除并返回列表中的第一个元素 |
Object removeLast() | 删除并返回列表中的最后一个元素 |
2.4 ArrayList与LinkedList性能对比
模拟10w
条数据指定插入第一位
,然后查询全部
,循环删除第一位
public class TestList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//测试ArrayList新增/查询/删除100000次
testList("ArrayList",list);
System.out.println("==============分割线===========");
//测试LinkedList新增/查询/删除100000次
list = new LinkedList<>();
testList("LinkedList",list);
}
public static void testList(String type,List<String> list) {
int maxTestCount = 100000;
//--------测试添加--------
long start = System.currentTimeMillis();
for (int i = 0; i < maxTestCount; i++) {
list.add(0, String.valueOf(i));
}
long end = System.currentTimeMillis();
System.out.println(type+" add cost time :" + (end - start));
//--------测试查询--------
start = System.currentTimeMillis();
for (int i = 0; i < maxTestCount; i++) {
list.get(i);
}
end = System.currentTimeMillis();
System.out.println(type+" get cost time :" + (end - start));
//--------测试删除--------
start = System.currentTimeMillis();
for (int i = maxTestCount; i > 0; i--) {
list.remove(0);
}
end = System.currentTimeMillis();
System.out.println(type+" remove cost time :" + (end - start));
}
}
执行结果如下:
ArrayList add cost time :985
ArrayList get cost time :3
ArrayList remove cost time :958
==============分割线===========
LinkedList add cost time :10
LinkedList get cost time :14157
LinkedList remove cost time :5
从上述结果分析如下:
- ArrayList插入、删除效率明显低于LinkedList
- ArrayList查询效率远远高于LinkedList
这个和数据结构
有关系:
- Arraylist
顺序表
,用数组
的方式实现。查询哪个元素只给出其下标即可,所以才说ArrayList随机访问多的场景比较合适。但是如果删除某个元素比如第 i 个元素,则要将 i 之后的元素都向前移一位以保证顺序表的正确,增加也是一样,要移动多个元素。要多次删除增加的话是很低效的。 - LinkedList是
双向链表
。只能从头节点开始逐步查询,没有直接根据下标查询的方法,需要遍历。但是,如果要增加后删除一个元素的话,只需要改变其前后元素的指向即可,不需要像ArrayList那样整体移动,所以才说多用于增删
多的场合
3. Map接口
- Map集合存储的是键值对
- Map集合的实现类:HashTable、LinkedHashMap、HashMap、TreeMap
Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作。使用这种方式存储数据的优点是查询指定元素的效率高。
3.1 HashMap集合类
基础了解:
- 1、键不可以重复,值可以重复;
- 2、底层使用哈希表实现;
- 3、线程不安全;
- 4、允许key为null,但只允许有一条记录为null,value也可以为null,允许多条记录为null;
从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示
3.2 常用方法如下
方法名 | 说 明 |
---|---|
Object put(Object key, Object val) | 以“键-值对”的方式进行存储 |
Object get (Object key) | 根据键返回相关联的值,如果不存在指定的键,返回null |
Object remove (Object key) | 删除由指定的键映射的“键-值对” |
int size() | 返回元素个数 |
Set keySet () | 返回键的集合 |
Collection values () | 返回值的集合 |
boolean containsKey (Object key) | 如果存在由指定的键映射的“键-值对”,返回true |
4. Set接口
Set继承于Collection接口,是一个不允许出现重复元素,并且无序的集合,主要有HashSet和TreeSet两大实现类。
在判断重复元素的时候,Set集合会调用元素的hashCode()和equal()方法来实现。
-
HashSet是哈希表结构,主要利用HashMap的key来存储元素,计算插入元素的hashCode来获取元素在集合中的位置;
-
TreeSet是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序;
Set集合框架结构:
4.1 Set常用方法
与List接口一样,Set接口也提供了集合操作的基本方法。
但与List不同的是,Set还提供了equals(Object o)和hashCode(),供其子类重写,以实现对集合中插入重复元素的处理
public interface Set<E> extends Collection<E> {
// A:添加功能
boolean add(E e);
boolean addAll(Collection<? extends E> c);
// B:删除功能
boolean remove(Object o);
boolean removeAll(Collection<?> c);
void clear();
// C:长度功能
int size();
// D:判断功能
boolean isEmpty();
boolean contains(Object o);
boolean containsAll(Collection<?> c);
boolean retainAll(Collection<?> c);
// E:获取Set集合的迭代器:
Iterator<E> iterator();
// F:把集合转换成数组
Object[] toArray();
<T> T[] toArray(T[] a);
//判断元素是否重复,为子类提高重写方法
boolean equals(Object o);
int hashCode();
}
4.2 HashSet
HashSet实现Set接口,底层由HashMap来实现,为哈希表结构,新增元素相当于HashMap的key,value默认为一个固定的Object。
当有元素插入的时候,会计算元素的hashCode值,将元素插入到哈希表对应的位置中来;
它继承于AbstractSet,实现了Set, Cloneable, Serializable接口。
- HashSet继承AbstractSet类,获得了Set接口大部分的实现,减少了实现此接口所需的工作,实际上是又继承了AbstractCollection类;
- HashSet实现了Set接口,获取Set接口的方法,可以自定义具体实现,也可以继承AbstractSet类中的实现;
- HashSet实现Cloneable,得到了clone()方法,可以实现克隆功能;
- HashSet实现Serializable,表示可以被序列化,通过序列化去传输,典型的应用就是hessian协议
具有如下特点:
- 不允许出现重复因素;
- 允许插入Null值;
- 元素无序(添加顺序和遍历顺序不一致);
- 线程不安全,若2个线程同时操作HashSet,必须通过代码实现同步;
4.3 HashSet基本操作
HashSet底层由HashMap实现,插入的元素被当做是HashMap的key,根据hashCode值来确定集合中的位置,由于Set集合中并没有下标的概念,所以并没有像List一样提供get()方法。当获取HashSet中某个元素时,只能通过遍历集合的方式进行equals()比较来实现;
5. 迭代器 Iterator
5.1 迭代器概述
在Java中,有很多的数据容器,对于这些的操作有很多的共性。Java采用了迭代器来为各种容器提供了公共的操作接口。这样使得对容器的遍历操作与其具体的底层实现相隔离,达到解耦的效果。
在Iterator接口中定义了三个方法:
5.2 使用迭代器
迭代器 的基本操作是 next 、hasNext 和 remove。
- 调用 next() 会返回迭代器的下一个元素,并且更新迭代器的状态。
- 调用 hasNext() 用于检测集合中是否还有元素。
- 调用 remove() 将迭代器返回的元素删除。
Iterator 类位于 java.util 包中,使用前需要引入它,语法格式如下:
import java.util.Iterator; // 引入 Iterator 类
Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 List
、Set
、Map
等集合。
演示示例如下:
List<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
for (Iterator iter = list.iterator(); iter.hasNext();) {
String str = iter.next();
System.out.println(str);
}
/*迭代器用于while循环
Iterator iter = list.iterator();
while(iter.hasNext()){
String str = iter.next();
System.out.println(str);
}*/
//遍历Set与遍历List一致
Set<String> set = new HashSet<>();
set.add("aa");
set.add("bb");
set.add("cc");
for (Iterator<String> iter = set.iterator(); iter.hasNext();) {
String str = iter.next();
System.out.println(str);
}
/*迭代器用于while循环
Iterator<String> iter = set.iterator();
while(iter.hasNext()){
String str = iter.next();
System.out.println(str);
}*/
//遍历Map
Map<String,Integer> map=new HashMap<>();
map.put("数学",90);
map.put("语文",88);
map.put("英语",90);
//第一种遍历方式
Set<Entry<String,Integer>> set=map.entrySet();
//map键值对是存放在节点里面,通过entrySet()获得键值对的集合
for(Iterator<Entry<String,Integer>> it=set.iterator();it.hasNext();)
{
Entry<String,Integer> s=it.next();
System.out.println(s.getKey()+"+"+s.getValue());
}
/*迭代器用于while循环
Iterator<Entry<String,Integer>> it=set.iterator();
while(iter.hasNext()){
Entry<String,Integer> s=it.next();
System.out.println(s.getKey()+"+"+s.getValue());
}*/
//第二种遍历方式,相当于遍历集合*
//相当于建立一个包含所有键的集合,
//再通过Map自带方法map.get(key)返回键对应的值,达到遍历效果
Set<String> set = map.keySet();
for(Iterator<String> iter=set.iterator();it.hasNext();)
{
String s=iter.next();
System.out.println(s+"+"+map.get(s));
}
/*迭代器用于while循环
Iterator<String> iter=set.iterator();
while(iter.hasNext()){
String s=iter.next();
System.out.println(s+"+"+map.get(s));
}*/
6. 泛型集合
Java中的泛型(generics)是JDK 5中引入的一个新特性,提供编译时类型安全检测机制,允许在编译时检测非法的类型。平常用到的大多数泛型写的程序都和处理集合有关,虽然泛型也可以用在其他地方,但是它的主要目的还是让我们可以写出有类型安全的集合。
在没有泛型功能之前,集合都写成Object类型,我们可以把任何类型的数据放入ArrayList中,就像ArrayList。 使用泛型后,可以指定ArrayList中可存储的对象类型。
泛型带来的好处不只是代码重用了,还包括类型安全,以及更好的可读性
定义泛型类如下:
class Hello<T>{
//省略其他代码
}
定义泛型方法如下:
public <T>void show(T t){
//省略其他代码
System.out.println(t);
}
public <T>T message(T t){
//省略其他代码
return t;
}
定义泛型接口如下:
interface Message<T>{
//省略其他代码
}
7. 泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
通配符基本使用
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。
此时只能接受数据,不能往该集合中存储数据。
举个例子大家理解使用即可:
public static void main(String[] args) {
List<Intger> list1 = new ArrayList<Integer>();
getElement(list1);
List<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(List<?> list){}
//?代表可以接收任意类型
注意:泛型不存在继承关系 Collection<Object> list = new ArrayList<String>();这种是错误的。
通配符高级使用----受限泛型
之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限和下限。
泛型的上限:
- 格式:
类型名称 <? extends 类 > 对象名称
- 意义:
只能接收该类型及其子类
泛型的下限:
- 格式:
类型名称 <? super 类 > 对象名称
- 意义:
只能接收该类型及其父类型
比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<Integer>();
List<String> list2 = new ArrayList<String>();
List<Number> list3 = new ArrayList<Number>();
List<Object> list4 = new ArrayList<Object>();
getElement1(list1);
getElement1(list2);//报错
getElement1(list3);
getElement1(list4);//报错
getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(List<? extends Number> list){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(List<? super Number> list){}
8. 包装类
在 Java的设计中提倡一种思想,即一切皆对象。但是从数据类型的划分中,我们知道 Java 中的数据类型分为基本数据类型和引用数据类型,但是基本数据类型怎么能够称为对象呢?于是 Java 为每种基本数据类型分别设计了对应的类,称之为包装类(Wrapper Classes),也有地方称为外覆类或数据类型类。
包装类和基本数据类型的关系如下表所示。
序号 | 基本数据类型 | 包装类 |
---|---|---|
1 | byte | Byte |
2 | short | Short |
3 | int | Integer |
4 | long | Long |
5 | char | Character |
6 | float | Float |
7 | double | Double |
8 | boolean | Boolean |
从上表中我们可以看出,除了 Integer 和 Character 定义的名称与基本数据类型定义的名称相差较大外,其它的 6 种类型的名称都是很好掌握的。
8.1 装箱和拆箱
在了解包装类后,下面介绍包装类的装箱与拆箱的概念。其实这两个概念本身并不难理解,基本数据类型转换为包装类的过程称为装箱,例如把 int 包装成 Integer 类的对象;包装类变为基本数据类型的过程称为拆箱,例如把 Integer 类的对象重新简化为 int。
手动实例化一个包装类称为手动拆箱装箱。Java 1.5 版本之前必须手动拆箱装箱,之后可以自动拆箱装箱,也就是在进行基本数据类型和对应的包装类转换时,系统将自动进行装箱及拆箱操作,不用在进行手工操作,为开发者提供了更多的方便。例如:
public class Demo {
public static void main(String[] args) {
int m = 500;
Integer obj = m; // 自动装箱
int n = obj; // 自动拆箱
System.out.println("n = " + n);
Integer obj1 = 500;
System.out.println("obj等价于obj1返回结果为" + obj.equals(obj1));
}
}
运行结果:
n = 500
obj等价于obj1返回结果为true
8.2 包装类的应用
下面我们讲解 8 个包装类的使用,下面是常见的应用场景。
1) 实现 int 和 Integer 的相互转换
可以通过 Integer 类的构造方法将 int 装箱,通过 Integer 类的 intValue 方法将 Integer 拆箱。例如:
public class Demo {
public static void main(String[] args) {
int m = 500;
Integer obj = new Integer(m); // 手动装箱
int n = obj.intValue(); // 手动拆箱
System.out.println("n = " + n);
Integer obj1 = new Integer(500);
System.out.println("obj等价于obj1的返回结果为" + obj.equals(obj1));
}
}
运行结果:
n = 500
obj等价于obj1的返回结果为true
一道关于int和Integer的面试题
public class Demo {
public static void main(String[] args) {
Integer a = new Integer(3);//创建一个新的对象
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
System.out.println(f1 == f2); //true
System.out.println(f3 == f4); //false
}
}
首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==
运算比较的不是值而是引用。
装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache是Integer的内部类,其代码如下所示:
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2
的结果是true
,而f3==f4
的结果是false
。
2) 将字符串转换为数值类型
在 Integer类中分别提供了以下方法:
Integer 类(String 转 int 型)
int parseInt(String s);
注意:使用上面的方法时,字符串中的数据必须由数字组成,否则转换时会出现程序错误。
下面为字符串转换为数值类型的例子:
public class Demo {
public static void main(String[] args) {
String str1 = "30";
String str2 = "30.3";
// 将字符串变为int型
int x = Integer.parseInt(str1);
// 将字符串变为float型
float f = Float.parseFloat(str2);
System.out.println("x = " + x + ";f = " + f);
}
}
其他的包装类也都一样通过parseXXX方法转换
3) 将整数转换为字符串
Integer 类有一个静态的 toString() 方法,可以将整数转换为字符串。例如:
public class Demo {
public static void main(String[] args) {
int m = 500;
String s = Integer.toString(m);
System.out.println("s = " + s);
}
}
9. Collections
9.1 常用功能
java.utils.Collections
是集合工具类,用来对集合进行操作。部分方法如下:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。public static void shuffle(List<?> list) 打乱顺序
:打乱集合顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。
示例代码如下:
public class CollectionsDemo {
public static void main(String[] args) {
//创建集合
List<String> list = new ArrayList<String>();
//增加10个不同单词
//list.add("this");
//list.add("is");
//list.add("collection");
//list.add("test");
//list.add("and");
//list.add("we");
//list.add("can");
//list.add("learn");
//list.add("how");
//list.add("to");
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, "this","is","collection","test","and","we",
"can","learn","how","to");
//打印输出集合中最大元素和最小元素
String strMax = (String) Collections.max(list);
String strMin = (String) Collections.min(list);
System.out.println("最大值:"+strMax);
System.out.println("最小值:"+strMin);
//按升序打印输出集合中所有元素
Collections.sort(list);
System.out.println("集合升序");
for(int i=0;i<list.size();i++)
{
System.out.println(list.get(i));
}
System.out.println(Collections.binarySearch(list, "this"));
//按降序打印输出集合中所有元素
Collections.reverse(list);
System.out.println("集合降序");
for(int i=0;i<list.size();i++)
{
System.out.println(list.get(i));
}
}
}
代码演示之后 ,发现我们的集合按照顺序进行了排列,可是这样的顺序是采用默认的顺序,如果想要指定顺序那该怎么办呢?
我们发现还有个方法没有讲,public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。接下来讲解一下指定规则的排列。
9.2 Comparator比较器
我们还是先研究这个方法
public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。
public class CollectionsDemo2 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
结果:
[aba, cba, nba, sba]
我们使用的是默认的规则完成字符串的排序,那么默认规则是怎么定义出来的呢?
说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式
- 一种是比较死板的采用
java.lang.Comparable
接口去实现 - 一种是灵活的当我需要做排序的时候在去选择的
java.util.Comparator
接口完成。
那么我们采用的public static <T> void sort(List<T> list)
这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用
public static <T> void sort(List<T> list,Comparator<? super T> )
方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:
-
public int compare(String o1, String o2)
:比较其两个参数的顺序。两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序,
则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)
如果要按照降序排序
则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)
操作如下:
public class CollectionsDemo3 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法 按照第一个单词的降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.charAt(0) - o1.charAt(0);
}
});
System.out.println(list);
}
}
结果如下:
[sba, nba, cba, aba]
9.3 简述Comparable和Comparator两个接口的区别
Comparable:
强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator:
强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
9.4 案例
创建一个学生类,存储到ArrayList集合中完成指定排序操作。
Student 初始类
public class Student{
private String name;
private int age;
public Student() {
}
public Student(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 "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试类:
public class Demo {
public static void main(String[] args) {
// 创建四个学生对象 存储到集合中
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose",18));
list.add(new Student("jack",16));
list.add(new Student("abc",16));
list.add(new Student("ace",17));
list.add(new Student("mark",16));
/*
让学生 按照年龄排序 升序
*/
//Collections.sort(list);
//要求 该list中元素类型 必须实现比较器Comparable接口
for (Student student : list) {
System.out.println(student);
}
}
}
发现,当我们调用Collections.sort()方法的时候 程序报错了。
原因:如果想要集合中的元素完成排序,那么必须要实现比较器Comparable接口。
重写compareTo(参数)方法来定义对象间的比较规则。
该方法要求返回一个整数,这个整数不关心具体的值,而是关注取值范围。
当返回值>0时,表示当前对象比参数给定的对象大。
当返回值<0时,表示当前对象比参数给定的对象小。
当返回值=0时,表示当前对象和参数给定的对象相等。
于是我们就完成了Student类的一个实现,如下:
public class Student implements Comparable<Student>{
....
@Override
public int compareTo(Student o) {
//return o.age-this.age //降序
return this.age-o.age;//升序
//年龄升序 年龄如果相同,则按照姓名首字母升序
//return this.age - o.age == 0 ?
//this.name.charAt(0) - o.name.charAt(0) : this.age - o.age;
}
}
再次测试,代码就OK 了效果如下:
Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}
Student{name='ace', age=17}
Student{name='rose', age=18}
9.5 扩展
如果在使用的时候,想要独立的定义规则去使用 可以采用Collections.sort(List list,Comparetor<T>
c)方式,自己定义规则:
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
//return o1.getAge()-o2.getAge();//以学生的年龄升序
return o2.getAge()-o1.getAge();//以学生的年龄降序
}
});
效果:
Student{name='rose', age=18}
Student{name='ace', age=17}
Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}
如果想要规则更多一些,可以参考下面代码:
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 年龄降序
int result = o2.getAge()-o1.getAge();//年龄降序
if(result==0){//第一个规则判断完了 下一个规则 姓名的首字母 升序
result = o1.getName().charAt(0)-o2.getName().charAt(0);
}
return result;
}
});
以上代码使用lambda表达式第一种写法简化如下:
Collections.sort(list, (o1,o2)-> {
// 年龄降序 年龄如果相同 按姓名的首字母升序
o2.getAge()-o1.getAge()==0?o1.getName().charAt(0)-o2.getName().charAt(0):o2.getAge()-o1.getAge());
以上代码使用lambda表达式第二种写法简化如下:
public class Demo {
public static void main(String[] args) {
// 创建四个学生对象 存储到集合中
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("rose",18));
list.add(new Student("jack",16));
list.add(new Student("abc",16));
list.add(new Student("ace",17));
list.add(new Student("mark",16));
/*
年龄降序 年龄如果相同 按姓名的首字母升序
*/
//lambda第二种编写方式 使用方法引用
Collections.sort(list,Demo::compare);
//要求 该list中元素类型 必须实现比较器Comparable接口
for (Student student : list) {
System.out.println(student);
}
}
//使用方法引用
public static int compare(Student o1,Student o2) {
return o2.getAge()-o1.getAge()==0 ? o1.getName().charAt(0)-o2.getName().charAt(0) : o2.getAge()-o1.getAge();
}
}
效果如下:
Student{name='rose', age=18}
Student{name='ace', age=17}
Student{name='abc', age=16}
Student{name='jack', age=16}
Student{name='mark', age=16}
10. 备注
Object 类是所有类的父类,其 equals 方法比较的是两个对象的引用指向的地址,hashCode 是一个本地方法,返回的是对象地址值。他们都是通过比较地址来比较对象是否相等的
使用HashMap集合,存储自定义的对象。 如果自定义的对象作为键(键不能重复),需要重写自定义类型的hashCode()和equals()方法。
主要原因是:当我们用自定义的类的作为HashMap的key时,如何判断这相同的类对象的唯一性,这涉及到HashMap的判断机制,先比较key的hashCode(),当哈希值相同时,再比较key的equals(),这两个是配套的。
所以重写 hashCode 方法是为了让我们能够正常使用 HashMap 等集合类,因为 HashMap 判断对象是否相等既要比较 hashCode 又要使用 equals 比较。而这样的实现是为了提高 HashMap 的效率。
重写一个类的hashcode()方法有以下注意事项:
- 若一个类重写了equals()方法,那么就应当重写hashcode()方法。
- 若两个对象的equals()方法比较为true,那么它们应当具有相同的hashcode值。
- 对于同一个对象而言,在内容没有发生改变的情况下,多次调用hashCode()方法应当总是返回相同的值。
- 对于两个对象equals()比较为false的,并不要求其hashCode值一定不同,但应尽量保证不同,这样可以提高散列表性能。
重写可以使用开发工具生成。