文章目录
- 1. 比较器
- 1.1 Comparable
- 实例:对自定义对象进行排序
- 1.2 Comparator
- 实例:对自定义对象进行排序
- 1.3 equals
- 1.3.1 equals介绍
- 1.3.2 == 详解
- 2. 迭代器
- 2.1 Iterator
- 2.2 ListIterator
- 3. 枚举
- 3.1 枚举访问
- 3.1 枚举细节
1. 比较器
比较器指的是集合存储的元素的特性,如果元素是可比较的则可以进行相应的排序,否则不行。
Java中常用的比较器有 Comparable
接口和 comparator
接口 ,下面我们来介绍一下这两个比较器。
1.1 Comparable
Comparable
接口被称为自然排序,也叫做内部比较器,对于 Comparable
接口来说,它往往是进行比较类需要实现的接口,它仅包含一个有 compareTo()
方法,只有一个参数,返回值为 int
,
- 其返回值大于 0 0 0 表示对象大于参数对象;
- 其返回值小于 0 0 0表示对象小于参数对象;
- 其返回值等于 0 0 0表示两者相等 。
之所以称之为自然排序,那是因为像 String, Integer
等类中都实现了Comparable
接口的 compareTo()
方法,所以如果对一个包含String, Integer
等基础类的列表进行排序的话,其可以按照数字大小或者字母顺序进行排序,就是因为它们都实现了该方法。比如 String
类中的 compareTo
方法实现如下:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
从上面可以看出,它称为内部比较器还是有原因的,首先,它存在于类的内部,其次,它只传入了一个数据与本类的数据进行内部的比较。
接下来我们使用 Comparable
比较器实现一个按照零食价格进行排序的例子。
实例:对自定义对象进行排序
我们首先定义一个零食对象,并将其实现 Comparable
接口中的 compareTo
方法,按照价格进行排序,代码如下:
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
class snack implements Comparable{
private String name;
private double price;
@Override
public int compareTo(Object o) {
//按照图书价格从小到大排序
if (o instanceof snack){
snack s = (snack) o;
// 调用Double类中重写的compare方法,也可以自己写,依据返回值比较大小
return Double.compare(this.price, s.price);
}
throw new RuntimeException("传入的数据类型不一致!");
}
}
然后创建几个对象并对其使用数组的sort方法进行排序,之后再输出各个元素进行查看。
public class Solution {
public static void main(String[] args) {
snack[] snacks = new snack[]{new snack("奥利奥", 6.5), new snack("巧乐兹", 3),
new snack("趣多多", 7), new snack("呀土豆", 5)};
Arrays.sort(snacks);
for (snack snack : snacks) {
System.out.println(snack);
}
}
}
输出结果如下:
snack(name=巧乐兹, price=3.0)
snack(name=呀土豆, price=5.0)
snack(name=奥利奥, price=6.5)
snack(name=趣多多, price=7.0)
1.2 Comparator
Comparator
被称作外部比较器,当元素的类型没实现 java.lang.Comparable
接口而又不方便修改代码,或者实现了java.lang.Comparable
接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator
的对象来排序。
对于Comparator
接口来说,它的实现者被称为比较器,它包含一个compare()
方法,有两个参数,返回值与Comparable
的compareTo()
方法一样,不同之处是Comparator
接口一般不会被集合元素类所实现,而是单独实现或者匿名内部类方式实现。
其实现的方法签名为 compare(Object o1,Object o2)
,也就是比较 o1,o2
的大小,
- 如果方法返回正整数,则表示
o1
大于o2
; - 如果返回 0 0 0,表示相等;
- 返回负整数,表示
o1
小于o2
。
实例:对自定义对象进行排序
由于我们使用外部比较器实现对象的比较,这里对象就不需要实现接口了,故对象的定义如下:
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
class snack{
private String name;
private double price;
}
然后我们在sort方法中写入匿名内部类的比较器,定义我们的比较规则,代码如下:
public class Solution {
public static void main(String[] args) {
snack[] snacks = new snack[]{new snack("奥利奥", 6.5), new snack("巧乐兹", 3),
new snack("趣多多", 7), new snack("呀土豆", 5)};
// 使用匿名内部类定义外部比较器
Arrays.sort(snacks, new Comparator<snack>() {
@Override
public int compare(snack o1, snack o2) {
return Double.compare(o1.getPrice(), o2.getPrice());
}
});
for (snack snack : snacks) {
System.out.println(snack);
}
}
}
1.3 equals
1.3.1 equals介绍
前面两个比较器的实现都可以用来进行排序,这里 equals
的实现只是用来判断是否相等的
在Java中,使用 ==
进行比较,比较的是两个引用对象的地址是否相等,很多时候这种比较并不能达到我们需要的目的,所以,这就出现了 equals
方法。
equals
方法是Object类的默认方法,即每个对象都能够调用该方法,当该对象没有重写此方法时,调用此方法使用的就是Object中的代码,Object中该方法的实现如下:
public boolean equals(Object obj) {
return (this == obj);
}
可以看到,Object中的该方法与 ==
起的效果是一样的,而String
等类中调用 equals
方法能够比较值的不同,那是因为 String
等类重写了该方法,同理,如果自定义的类中也需要比较不同值,那么可以考虑重写 equals
方法。
1.3.2 == 详解
在使用基本数据类型 boolean,byte,char,short,int,long,float,double
进行比较时,如果使用 ==
,比价的是其值的大小。
那么,为什么下面的代码输出是 true
呢?
public class Solution {
public static void main(String[] args) {
// 自动装箱
Integer a = 3;
Integer b = 3;
System.out.println(a == b); //输出true
}
}
首先,我们来了解以下自动装箱和自动拆箱的相关知识。
自动装箱就是Java编译器在需要的时候,自动将原始数据类型转换为对应的包装类类型,例如将int
类型的值装箱成 Integer
类型的对象。
自动拆箱就是Java编译器在需要的时候,自动将包装类型对象转换为对应的基础数据类型,例如将Integer
类型的对象装箱成 int
类型的值。举例的话如下:
Integer a = 10; // 自动装箱
int b = a; // 自动拆箱
了解了自动装箱后,应该明白了上述代码的 Integer a = 3;Integer b = 3;
已经属于两个对象了,为什么使用 ==
会相等呢?
那是因为内部人员已经将 [-128, 127]
之间的整数装箱完毕,当程序中使用该范围之间的整数时,无需装箱直接取用自动装箱池中的对象即可,这也就表示 Integer a = 3;Integer b = 3;
其实装箱的都是已经装箱完毕的同一个对象,所以使用 ==
时返回的是 true
。
完整的流程如下:
自动装箱时首先判断i
值是否在[-128,127]
这个区间中,如果在这个区间中,则直接从IntegerCache.cache
缓存中获取指定数字的包装类,IntegerCache
内部实现了一个Integer
的静态常量数组,在类加载的时候,执行static静态块进行初始化[-128,127]
之间的Integer
对象, 存放到cache
数组中,cache
数组属于常量,存放在java的方法区中,这样调用时会使用缓存池中的对象,多次调用会取得同一个对象的引用,不在这个区间中则new出一个新的包装类。
于是,像下述代码,直接new
的一个对象,并未使用缓存中的对象,判断是肯定不相等的,
public class Solution {
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3;
System.out.println(a == b); // 输出false
}
}
2. 迭代器
迭代器是一种对象,它允许按顺序访问集合中的元素,而不需要知道集合的底层结构。通过使用迭代器,我们可以遍历集合并访问其中的元素,而无需关心集合的具体实现方式。
Java 提供了 Iterator
接口作为迭代器的基础接口。该接口定义了一组用于访问集合元素的方法,包括 hasNext
、next
和 remove
等,该接口下还有 ListIterator
和 Spliterator
等子接口,接下来我们对 Iterator, ListIterator
进行介绍。
2.1 Iterator
Iterator
接口是所有迭代器的祖先,它是 Java 操作集合元素的标准方式之一。它提供了对集合元素进行遍历和删除的基本方法,Collection
中所有的集合都实现了 Iterator
接口,该接口定义了几个方法,其中最常用的方法如下:
函数签名 | 功能 |
---|---|
boolean hasNext() | 如果迭代器可以迭代下一个元素,返回 true |
E next() | 返回迭代器的下一个元素 |
void remove() | 移除当前迭代器的元素 |
当使用 Iterator
进行迭代的时候,最开始其位于第一个元素的前面,当使用一次 next()
函数后,其到达第一个元素后、第二个元素前,其并不直接指向元素,如下图所示。
当我们需要进行迭代的时候,可以按照如下方式进行迭代:
public class Solution {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("java");
arrayList.add("python");
arrayList.add("C");
arrayList.add("C#");
arrayList.add("javascript");
// 使用迭代器
Iterator<String> iterator = arrayList.iterator();
while(iterator.hasNext()){
String item = iterator.next();
System.out.println(item);
}
}
}
当然,我们也可以使用增强 for
循环来进行循环迭代,它也是使用的迭代器来进行遍历,而且简化了迭代器的使用,不需要显式地使用迭代器,如下:
public class Solution {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("java");
arrayList.add("python");
arrayList.add("C");
arrayList.add("C#");
arrayList.add("javascript");
// 增强for进行遍历
for (String item : arrayList) {
System.out.println(item);
}
}
}
2.2 ListIterator
ListIterator
接口继承自 Iterator
接口,它专门用于遍历 List
集合的元素,Iterator
接口只能从前往后遍历集合元素,而 ListIterator
接口提供了从前往后以及从后往前遍历集合元素的方法,还可以在遍历的过程中添加、修改、删除元素。
ListIterator
接口的主要方法如下:
函数签名 | 功能 |
---|---|
boolean hasNext() | 如果迭代器可以迭代下一个元素,返回 true |
E next() | 返回迭代器的下一个元素 |
void remove() | 移除当前迭代器的元素 |
boolean hasPrevious() | 如果迭代器可以迭代前一个元素,返回 true |
E previous() | 返回迭代器的前一个元素 |
可以使用上面的示例为:
public class Solution {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("java");
arrayList.add("python");
arrayList.add("C");
arrayList.add("C#");
arrayList.add("javascript");
System.out.println("正序:");
ListIterator<String> listIterator = arrayList.listIterator();
while(listIterator.hasNext()) {
String item = listIterator.next();
System.out.println(item);
}
System.out.println("逆序:");
while(listIterator.hasPrevious()) {
String item = listIterator.previous();
System.out.println(item);
}
}
}
3. 枚举
Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一年的 12 个月份,一个星期的 7 天,方向有东南西北等。
Java 枚举类使用 enum
关键字来定义,各个常量使用逗号 ,
来分割。
一个简单的枚举定义如下:
enum Direction{
WEST, EAST, SOUTH, NORTH
}
即使用 enum
关键字即可定义。
3.1 枚举访问
枚举常用的方法如下:
函数签名 | 功能 |
---|---|
values() | 返回枚举类中所有的值 |
ordinal() | 找到每个枚举常量的索引 |
valueOf() | 返回指定字符串值的枚举常量 |
上述方法的使用如下:
public class Solution {
public static void main(String[] args) {
// 调用 values()
Direction[] arr = Direction.values();
// 迭代枚举
for (Direction direction : arr) {
// 查看索引
System.out.println(direction + " at index " + direction.ordinal());
}
// 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException
System.out.println(Direction.valueOf("WEST"));
}
}
代码执行的结果如下:
WEST at index 0
EAST at index 1
SOUTH at index 2
NORTH at index 3
WEST
定义了上述枚举类机器变量后,如果需要对其进行单独的访问,除了使用 valueOf
函数,还可以直接访问其成员,代码如下:
public class Solution {
public static void main(String[] args) {
Direction d1 = Direction.EAST;
System.out.println(d1);
}
}
输出结果为:
EAST
3.1 枚举细节
看过了上面的代码后,不知道你们会不会问,为什么枚举的变量可以使用类似成员变量的访问形式 Direction.EAST
来进行访问呢?
其实,每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final
的。上面的枚举类 Direction 转化在内部类中实现为:
class Direction{
public static final Direction WEST = new Direction();
public static final Direction EAST = new Direction();
public static final Direction NORTH = new Direction();
public static final Direction SOUTH = new Direction();
}
不仅如此,枚举类也可以在类的内部进行使用,比如:
public class Test {
enum Direction {
WEST, EAST, SOUTH, NORTH
}
// 执行输出结果
public static void main(String[] args) {
Direction d1 = Direction.WEST;
System.out.println(d1);
}
}
了解了枚举类中的每个变量都是通过无参构造方法而来的后,我们可以对每个枚举的变量添加参数,比如:
enum Direction{
WEST("西方"),
EAST("东方"),
SOUTH("南方"),
NORTH("北方");
private String ChineseName;
private Direction(String name){
this.ChineseName = name;
}
public String getChineseName() {
return ChineseName;
}
}
了解到枚举的变量本质后,上述代码的 WEST("西方")
一行其实就是 public static final Direction WEST = new Direction("西方");
,相当于就是初始化一个有参构造方法,所以我们增加了一个变量 ChineseName
,用来记录我们传入的中文名字,如果想要对这个变量进行访问,同样,按照函数的形式进行访问即可。
public class Solution {
public static void main(String[] args) {
Direction d1 = Direction.EAST;
String name = d1.getChineseName();
System.out.println(name); //输出 "东方"
}
}
}
}
了解到枚举的变量本质后,上述代码的 `WEST("西方")` 一行其实就是 `public static final Direction WEST = new Direction("西方");` ,相当于就是初始化一个有参构造方法,所以我们增加了一个变量 `ChineseName` ,用来记录我们传入的中文名字,如果想要对这个变量进行访问,同样,按照函数的形式进行访问即可。
```java
public class Solution {
public static void main(String[] args) {
Direction d1 = Direction.EAST;
String name = d1.getChineseName();
System.out.println(name); //输出 "东方"
}
}