由浅入深,大道至简。
一、炼精化气:int数组冒泡排序
什么,听说你修真忘了秘籍?这就带你回忆回忆,冒泡排序图解:
理解了冒泡排序流程之后,我们随手写一个int类型数组的冒泡排序:
public static void main(String[] args) {
int[] array = {5, 6, 7, 12, 36, 9, 10, 4};
for (int i = 0; i < array.length; i++) {
for (int j = i; j < array.length; j++) {
if (array[i] > array[j]) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
System.out.println(Arrays.toString(array));
}
控制台输出结果:
[4, 5, 6, 7, 9, 10, 12, 36]
Process finished with exit code 0
输出结果与我们预想的一致。
but,请思考一下,如果说我们要给String类型的字符串进行冒泡排序,又该怎么解决呢?
String[] array = {"100", "13", "21", "12", "43", "90", "01", "100"};
我们先试着把int类型的数字变成字符串,这样就能获得一个字符串数组,然后通过转型,又将字符串变成数字,像之前那样进行比较。
public static void main(String[] args) {
String[] array = {"100", "13", "21", "12", "43", "90", "01", "100"};
for (int i = 0; i < array.length; i++) {
for (int j = i; j < array.length; j++) {
int a = Integer.parseInt(array[i]);// String转int类型
int b = Integer.parseInt(array[j]);
if (a > b) {// 比较int类型的两个数
String temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
System.out.println(Arrays.toString(array));
}
输出结果
[01, 12, 13, 21, 43, 90, 100, 100]
Process finished with exit code 0
我猜你会想,这不是脱了裤子放屁,最后还是转成了int进行比较吗?
别着急,这是为之后转换真正的字符串进行铺垫。
二、炼气化神:String数组使用API进行比较——compareTo
我们直接查看String给我们提供的比较API源码:
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;
}
通过源码我们不难发现,原理和之前我们的写法有着惊人的相似!
API首先获取两个字符串的长度,通过Math.min函数取其中最小的长度,将字符串转换为char类型的数组,再将char数组进行遍历比较,由此制定了String类型的比较规则。
我们使用compareTo方法玩一把:
public static void main(String[] args) {
String[] array = {"dcba", "acdb", "cadb", "cabd", "abcd", "adcb", "cbad", "dabc"};
for (int i = 0; i < array.length; i++) {
for (int j = i; j < array.length; j++) {
if (array[i].compareTo(array[j]) > 0) {
String temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
System.out.println(Arrays.toString(array));
}
输出结果:
[abcd, acdb, adcb, cabd, cadb, cbad, dabc, dcba]
Process finished with exit code 0
这样的排序结果还是令人比较满意的。按照首字母排列。
那我们再多测试几组数据看看(此处忽略代码结构,仅观察参数和输出结果):
String[] array = {"a", "c", "d", "b", "cda", "ba", "cd", "ab"};
[a, ab, b, ba, c, cd, cda, d]
Process finished with exit code 0
String[] array = {"100", "13", "21", "12", "43", "90", "01", "100"};
[01, 100, 100, 12, 13, 21, 43, 90]
Process finished with exit code 0
我们发现结果开始有点不对劲!似乎不是我们期望的结果了。
由此我们需要重新了解compareTo方法的特性!
通过观察测试结果,再结合compareTo方法的源码,我们不难看出,这种比较方式,类似最左前缀原理。从左到右,按照字符ASCALL码大小进行排序,而完全与字符串的长度无关,也就是字符串的长度不被考虑进入比较范围。
因此,在以后使用此方法的过程中,一定要注意此方法不适用于比较长度不一的数字型字符串。
现在我们搞明白了字符串的比较规则,下一步我们要研究研究如何比较对象。
三、炼神还虚:将对象排序——实现Comparable接口
什么?你没有对象?别闹!
通常情况下,当我们谈到给对象排序,脑子里首先想到的应该是让对象实体实现Comparable接口,然后重写compareTo方法。
说干就干,我们指定指定以id进行排序:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Comparable {
private String id;
private String username;
private String password;
private int age;
@Override
public int compareTo(Object o) {
return o instanceof User ? this.id.compareTo(((User) o).id) : 0;
}
}
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
// id username password age
userList.add(new User("009", "admin5", "123123", 12));
userList.add(new User("005", "admin4", "321332", 12));
userList.add(new User("003", "admin3", "123123", 12));
userList.add(new User("004", "admin2", "123123", 15));
userList.add(new User("002", "admin1", "123123", 16));
Collections.sort(userList);
userList.forEach(user -> System.out.println(user));
}
输出结果:
User(id=002, username=admin1, password=123123, age=16)
User(id=003, username=admin3, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=005, username=admin4, password=321332, age=12)
User(id=009, username=admin5, password=123123, age=12)
Process finished with exit code 0
没错了,是我们要的根据id排序!
接下来,我们来仿写一个Comparable接口。
四、炼虚合道:仿写Comparable接口
先写一个接口:
public interface MyComparable<T> {
int myCompareTo(T o);
}
给实体类指定比较规则:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements MyComparable {
private String id;
private String username;
private String password;
private int age;
@Override
public int myCompareTo(Object o) {
return o instanceof User ? this.id.compareTo(((User) o).id) : 0;
}
}
测试自定义排序:
public class Test {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
// id username password age
userList.add(new User("009", "admin5", "123123", 12));
userList.add(new User("005", "admin4", "321332", 12));
userList.add(new User("003", "admin3", "123123", 12));
userList.add(new User("004", "admin2", "123123", 15));
userList.add(new User("002", "admin1", "123123", 16));
mysort(userList);// 排序
userList.forEach(user -> System.out.println(user));
}
// 指定排序方式
public static <T extends MyComparable<? super T>> void mysort(List<T> users) {
for (int i = 0; i < users.size(); i++) {
for (int j = i; j < users.size(); j++) {
T a = users.get(i);
T b = users.get(j);
if (a.myCompareTo(b) > 0)
Collections.swap(users, i, j);
}
}
}
}
输出结果:
User(id=002, username=admin1, password=123123, age=16)
User(id=003, username=admin3, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=005, username=admin4, password=321332, age=12)
User(id=009, username=admin5, password=123123, age=12)
Process finished with exit code 0
不错不错, 自定义的Comparable接口也能满足我们的需求。
可即便是这样的排序方式,不禁让人产生思考:若是换一种排序规则,岂不是又要修改代码重写compareTo方法?
五、超凡入圣:将对象排序——实现Comparator接口
为了解决上面抛出的问题,我们可以使用Comparator接口来解决。
使用匿名内部类可以灵活地设置排序规则
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
// id username password age
userList.add(new User("009", "admin5", "123123", 12));
userList.add(new User("005", "admin4", "321332", 12));
userList.add(new User("003", "admin3", "123123", 12));
userList.add(new User("004", "admin2", "123123", 15));
userList.add(new User("002", "admin1", "123123", 16));
// 排序
userList.sort(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return o1.getId().compareTo(o2.getId());
}
});
userList.forEach(user -> System.out.println(user));
}
输出结果:
User(id=002, username=admin1, password=123123, age=16)
User(id=003, username=admin3, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=005, username=admin4, password=321332, age=12)
User(id=009, username=admin5, password=123123, age=12)
Process finished with exit code 0
使用Comparator接口可以不用再到实体类中指定比较规则,直接使用匿名内部类的方式指定排序规则,相较于Comparable接口更加灵活。
可这样的做法,只适用于偶尔一两次的排序,你能明白我的意思吗?如果在项目中大量地用到了排序,每次排序我们都要去写一个内部类指定排序规则,这样难免写出很多相同代码,使整个程序看起来臃肿,为了应对这样的场景,我们接着往下看。
六、因果不沾:Comparator接口——抽取多种排序规则
如果在我们的需求中,存在多种排序方式,可以直接在实体类中写内部类,通过内部类实现Comparator接口,指定多种比较规则。而在我们排序的时候,直接创建内部类对象即可。
在User实体类中写俩内部类:SortUserById 和 SortUserByAge,实现Comparator接口
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String id;
private String username;
private String password;
private int age;
// 根据id排序
class SortUserById implements Comparator<User> {
@Override
public int compare(User o1, User o2) {
return o1.getId().compareTo(o2.getId());
}
}
// 根据年龄排序
class SortUserByAge implements Comparator<User> {
@Override
public int compare(User o1, User o2) {
return o1.getAge() - o2.getAge();
}
}
}
在测试类中,我们直接new内部类实现排序
public class Test {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
// id username password age
userList.add(new User("009", "admin5", "123123", 12));
userList.add(new User("005", "admin4", "321332", 12));
userList.add(new User("003", "admin3", "123123", 12));
userList.add(new User("004", "admin2", "123123", 15));
userList.add(new User("002", "admin1", "123123", 16));
//userList.sort(new User().new SortUserById());// 通过id排序
userList.sort(new User().new SortUserByAge());// 通过年龄排序
userList.forEach(user -> System.out.println(user));
}
}
输出结果:
1、通过id排序:
User(id=002, username=admin1, password=123123, age=16)
User(id=003, username=admin3, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=005, username=admin4, password=321332, age=12)
User(id=009, username=admin5, password=123123, age=12)
Process finished with exit code 0
2、通过年龄排序:
User(id=009, username=admin5, password=123123, age=12)
User(id=005, username=admin4, password=321332, age=12)
User(id=003, username=admin3, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=002, username=admin1, password=123123, age=16)
Process finished with exit code 0
这样一次指定多种排序方式,在需要的时候直接创建排序规则,比之前的方法更加灵活和便捷!
然后紧接着下一步,我们要解决多重排序,比如:先通过年龄排序,同年龄的情况下,再按照id排序。
七、万劫不灭:Comparator接口——多重排序
多重排序,我们只需要新建一个内部类:
// 根据年龄,再根据id排序
class SortUserByAgeThenId implements Comparator<User>{
@Override
public int compare(User o1, User o2) {
int result = o1.getAge() - o2.getAge();
if(result == 0) // 若age相等,则比较id
result = o1.getId().compareTo(o2.getId());
return result;
}
}
测试方法同上,我们再看测试结果!
User(id=003, username=admin3, password=123123, age=12)
User(id=005, username=admin4, password=321332, age=12)
User(id=009, username=admin5, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=002, username=admin1, password=123123, age=16)
Process finished with exit code 0
现在是不是完美了!
八、无所不知,无所不能:仿写Comparator
既然知道了如何使用Comparator接口,自然要自己手写一把自定义Comparator才过瘾!
老规矩,先写接口:
@FunctionalInterface
public interface MyComparator<T> {
int myCompare(T o1, T o2);
}
接着写实体类和排序规则:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String id;
private String username;
private String password;
private int age;
// 根据id排序
class SortUserById implements MyComparator<User> {
@Override
public int myCompare(User o1, User o2) {
return o1.getId().compareTo(o2.getId());
}
}
// 根据年龄排序
class SortUserByAge implements MyComparator<User> {
@Override
public int myCompare(User o1, User o2) {
return o1.getAge() - o2.getAge();
}
}
// 根据年龄,再根据id排序
class SortUserByAgeThenId implements MyComparator<User> {
@Override
public int myCompare(User o1, User o2) {
int result = o1.getAge() - o2.getAge();
if (result == 0)
result = o1.getId().compareTo(o2.getId());
return result;
}
}
}
最后是测试类:
public class Test {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
// id username password age
userList.add(new User("009", "admin5", "123123", 12));
userList.add(new User("005", "admin4", "321332", 12));
userList.add(new User("003", "admin3", "123123", 12));
userList.add(new User("004", "admin2", "123123", 15));
userList.add(new User("002", "admin1", "123123", 16));
mysort(userList, new User().new SortUserByAgeThenId());// 自定义排序
userList.forEach(user -> System.out.println(user));
}
// 自定义排序
public static <T> void mysort(List<T> users, MyComparator<T> comparator) {
for (int i = 0; i < users.size(); i++) {
for (int j = i; j < users.size(); j++) {
T a = users.get(i);
T b = users.get(j);
if (comparator.myCompare(a, b) > 0)
Collections.swap(users, i, j);
}
}
}
}
输出结果:
User(id=003, username=admin3, password=123123, age=12)
User(id=005, username=admin4, password=321332, age=12)
User(id=009, username=admin5, password=123123, age=12)
User(id=004, username=admin2, password=123123, age=15)
User(id=002, username=admin1, password=123123, age=16)
Process finished with exit code 0
很简单有木有?接下来是优化的写法。
九、天道不灭,圣人不死:使用lambda表达式,简化Comparator写法
这里我们优化的是匿名内部类。
在上文中,我们有一段代码是这样写的:
// 排序
userList.sort(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return o1.getId().compareTo(o2.getId());
}
});
在Java 8中,我们可以使用函数式编程,Lambda表达式在Java语言中引入了一个操作符**“->”**,该操作符被称为Lambda操作符或箭头操作符。它将Lambda分为两个部分:
- 左侧:指定了Lambda表达式需要的所有参数
- 右侧:指定了Lambda体,即Lambda表达式要执行的功能。
于是乎,上面的方法,等同于下面的方法。
userList.sort((o1, o2) -> {
return o1.getId().compareTo(o2.getId());
});
我们继续简化,去掉花括号:
userList.sort((o1, o2) -> o1.getId().compareTo(o2.getId()));
代码逐渐优雅起来,直接压缩成一句代码。
飞升——羽化登仙
掌握了上面的一系列比较方法,在应对不同场景的比较时,基本上可以做到游刃有余,立于不败之地!
学废没?反正我是学废了~~略略略