提交 fd04ace1 编写于 作者: 沉默王二's avatar 沉默王二 💬

新增三篇

上级 98bbed18
## 马蜂窝一面:Comparable和Comparator有什么区别?
那天,小二去马蜂窝面试,面试官老王一上来就甩给了他一道面试题:请问Comparable和Comparator有什么区别?小二差点笑出声,因为三年前,也就是 2021 年,他在《Java 程序员进阶之路》专栏上看到过这题😆。
*PS:星标这种事,只能求,不求没效果,come on。《Java 程序员进阶之路》在 GitHub 上已经收获了 565 枚星标,小伙伴们赶紧去点点了,冲 600*
>https://github.com/itwanger/toBeBetterJavaer
Comparable 和 Comparator 是 Java 的两个接口,从名字上我们就能够读出来它们俩的相似性:以某种方式来比较两个对象。但它们之间到底有什么区别呢?请随我来,打怪进阶喽!
### 01、Comparable
Comparable 接口的定义非常简单,源码如下所示。
```java
public interface Comparable<T> {
int compareTo(T t);
}
```
如果一个类实现了 Comparable 接口(只需要干一件事,重写 `compareTo()` 方法),就可以按照自己制定的规则将由它创建的对象进行比较。下面给出一个例子。
```java
public class Cmower implements Comparable<Cmower> {
private int age;
private String name;
public Cmower(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public int compareTo(Cmower o) {
return this.getAge() - o.getAge();
}
public static void main(String[] args) {
Cmower wanger = new Cmower(19,"沉默王二");
Cmower wangsan = new Cmower(16,"沉默王三");
if (wanger.compareTo(wangsan) < 0) {
System.out.println(wanger.getName() + "比较年轻有为");
} else {
System.out.println(wangsan.getName() + "比较年轻有为");
}
}
}
```
在上面的示例中,我创建了一个 Cmower 类,它有两个字段:age 和 name。Cmower 类实现了 Comparable 接口,并重写了 `compareTo()` 方法。
程序输出的结果是“沉默王三比较年轻有为”,因为他比沉默王二小三岁。这个结果有什么凭证吗?
凭证就在于 `compareTo()` 方法,该方法的返回值可能为负数,零或者正数,代表的意思是该对象按照排序的规则小于、等于或者大于要比较的对象。如果指定对象的类型与此对象不能进行比较,则引发 `ClassCastException` 异常(自从有了泛型,这种情况就少有发生了)。
### 02、Comparator
Comparator 接口的定义相比较于 Comparable 就复杂的多了,不过,核心的方法只有两个,来看一下源码。
```java
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
```
第一个方法 `compare(T o1, T o2)` 的返回值可能为负数,零或者正数,代表的意思是第一个对象小于、等于或者大于第二个对象。
第二个方法 `equals(Object obj)` 需要传入一个 Object 作为参数,并判断该 Object 是否和 Comparator 保持一致。
有时候,我们想让类保持它的原貌,不想主动实现 Comparable 接口,但我们又需要它们之间进行比较,该怎么办呢?
Comparator 就派上用场了,来看一下示例。
1)原封不动的 Cmower 类。
```java
public class Cmower {
private int age;
private String name;
public Cmower(int age, String name) {
this.age = age;
this.name = name;
}
}
```
(说好原封不动,getter/setter 吃了啊)
Cmower 类有两个字段:age 和 name,意味着该类可以按照 age 或者 name 进行排序。
2)再来看 Comparator 接口的实现类。
```java
public class CmowerComparator implements Comparator<Cmower> {
@Override
public int compare(Cmower o1, Cmower o2) {
return o1.getAge() - o2.getAge();
}
}
```
按照 age 进行比较。当然也可以再实现一个比较器,按照 name 进行自然排序,示例如下。
```java
public class CmowerNameComparator implements Comparator<Cmower> {
@Override
public int compare(Cmower o1, Cmower o2) {
if (o1.getName().hashCode() < o2.getName().hashCode()) {
return -1;
} else if (o1.getName().hashCode() == o2.getName().hashCode()) {
return 0;
}
return 1;
}
}
```
3)再来看测试类。
```java
Cmower wanger = new Cmower(19,"沉默王二");
Cmower wangsan = new Cmower(16,"沉默王三");
Cmower wangyi = new Cmower(28,"沉默王一");
List<Cmower> list = new ArrayList<>();
list.add(wanger);
list.add(wangsan);
list.add(wangyi);
list.sort(new CmowerComparator());
for (Cmower c : list) {
System.out.println(c.getName());
}
```
创建了三个对象,age 不同,name 不同,并把它们加入到了 List 当中。然后使用 List 的 `sort()` 方法进行排序,来看一下输出的结果。
```
沉默王三
沉默王二
沉默王一
```
这意味着沉默王三的年纪比沉默王二小,排在第一位;沉默王一的年纪比沉默王二大,排在第三位。和我们的预期完全符合。
### 03、到底该用哪一个呢?
通过上面的两个例子可以比较出 Comparable 和 Comparator 两者之间的区别:
- 一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。
- 一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。
- Comparable 接口在 `java.lang` 包下,而 `Comparator` 接口在 `java.util` 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。
举个不恰当的例子。我想从洛阳出发去北京看长城,体验一下好汉的感觉,要么坐飞机,要么坐高铁;但如果是孙悟空的话,翻个筋斗就到了。我和孙悟空之间有什么区别呢?孙悟空自己实现了 Comparable 接口(他那年代也没有飞机和高铁,没得选),而我可以借助 Comparator 接口(现代化的交通工具)。
------
好了,关于 Comparable 和 Comparator 我们就先聊这么多。总而言之,如果对象的排序需要基于自然顺序,请选择 `Comparable`,如果需要按照对象的不同属性进行排序,请选择 `Comparator`
\ No newline at end of file
## 手撸了一个Java的不可变对象,很哇塞!
>二哥,你能给我说说为什么 String 是 immutable 类(不可变对象)吗?我想研究它,想知道为什么它就不可变了,这种强烈的愿望就像想研究浩瀚的星空一样。但无奈自身功力有限,始终觉得雾里看花终隔一层。二哥你的文章总是充满趣味性,我想一定能够说明白,我也一定能够看明白,能在接下来写一写吗?
收到读者小 R 的私信后,我就总感觉自己有一种义不容辞的责任,非要把 immutable 类说明白不可!
*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 523 枚星标,铁子们赶紧去点点了,冲 600 star*
>https://github.com/itwanger/toBeBetterJavaer
### 01、什么是不可变类
一个类的对象在通过构造方法创建后如果状态不会再被改变,那么它就是一个不可变(immutable)类。它的所有成员变量的赋值仅在构造方法中完成,不会提供任何 setter 方法供外部类去修改。
还记得《神雕侠侣》中小龙女的古墓吗?随着那一声巨响,仅有的通道就被无情地关闭了。别较真那个密道,我这么说只是为了打开你的想象力,让你对不可变类有一个更直观的印象。
自从有了多线程,生产力就被无限地放大了,所有的程序员都爱它,因为强大的硬件能力被充分地利用了。但与此同时,所有的程序员都对它心生忌惮,因为一不小心,多线程就会把对象的状态变得混乱不堪。
为了保护状态的原子性、可见性、有序性,我们程序员可以说是竭尽所能。其中,synchronized(同步)关键字是最简单最入门的一种解决方案。
假如说类是不可变的,那么对象的状态就也是不可变的。这样的话,每次修改对象的状态,就会产生一个新的对象供不同的线程使用,我们程序员就不必再担心并发问题了。
### 02、常见的不可变类
提到不可变类,几乎所有的程序员第一个想到的,就是 String 类。那为什么 String 类要被设计成不可变的呢?
1)常量池的需要
字符串常量池是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串在常量池中不存在,那么就创建一个;假如已经存,就不会再创建了,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。
2)hashCode 的需要
因为字符串是不可变的,所以在它创建的时候,其 hashCode 就被缓存了,因此非常适合作为哈希值(比如说作为 HashMap 的键),多次调用只返回同一个值,来提高效率。
3)线程安全
就像之前说的那样,如果对象的状态是可变的,那么在多线程环境下,就很容易造成不可预期的结果。而 String 是不可变的,就可以在多个线程之间共享,不需要同步处理。
因此,当我们调用 String 类的任何方法(比如说 `trim()``substring()``toLowerCase()`)时,总会返回一个新的对象,而不影响之前的值。
```java
String cmower = "沉默王二,一枚有趣的程序员";
cmower.substring(0,4);
System.out.println(cmower);// 沉默王二,一枚有趣的程序员
```
虽然调用 `substring()` 方法对 cmower 进行了截取,但 cmower 的值没有改变。
除了 String 类,包装器类 Integer、Long 等也是不可变类。
### 03、手撸不可变类
看懂一个不可变类也许容易,但要创建一个自定义的不可变类恐怕就有点难了。但知难而进是我们作为一名优秀的程序员不可或缺的品质,正因为不容易,我们才能真正地掌握它。
接下来,就请和我一起,来自定义一个不可变类吧。一个不可变诶,必须要满足以下 4 个条件:
1)确保类是 final 的,不允许被其他类继承。
2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。
3)不要提供任何 setter 方法。
4)如果要修改类的状态,必须返回一个新的对象。
按照以上条件,我们来自定义一个简单的不可变类 Writer。
```java
public final class Writer {
private final String name;
private final int age;
public Writer(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
```
Writer 类是 final 的,name 和 age 也是 final 的,没有 setter 方法。
OK,据说这个作者分享了很多博客,广受读者的喜爱,因此某某出版社找他写了一本书(Book)。Book 类是这样定义的:
```java
public class Book {
private String name;
private int price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
```
2 个字段,分别是 name 和 price,以及 getter 和 setter,重写后的 `toString()` 方法。然后,在 Writer 类中追加一个可变对象字段 book。
```java
public final class Writer {
private final String name;
private final int age;
private final Book book;
public Writer(String name, int age, Book book) {
this.name = name;
this.age = age;
this.book = book;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public Book getBook() {
return book;
}
}
```
并在构造方法中追加了 Book 参数,以及 Book 的 getter 方法。
完成以上工作后,我们来新建一个测试类,看看 Writer 类的状态是否真的不可变。
```java
public class WriterDemo {
public static void main(String[] args) {
Book book = new Book();
book.setName("Web全栈开发进阶之路");
book.setPrice(79);
Writer writer = new Writer("沉默王二",18, book);
System.out.println("定价:" + writer.getBook());
writer.getBook().setPrice(59);
System.out.println("促销价:" + writer.getBook());
}
}
```
程序输出的结果如下所示:
```java
定价Book{name='Web全栈开发进阶之路', price=79}
促销价Book{name='Web全栈开发进阶之路', price=59}
```
糟糕,Writer 类的不可变性被破坏了,价格发生了变化。为了解决这个问题,我们需要为不可变类的定义规则追加一条内容:
如果一个不可变类中包含了可变类的对象,那么就需要确保返回的是可变对象的副本。也就是说,Writer 类中的 `getBook()` 方法应该修改为:
```java
public Book getBook() {
Book clone = new Book();
clone.setPrice(this.book.getPrice());
clone.setName(this.book.getName());
return clone;
}
```
这样的话,构造方法初始化后的 Book 对象就不会再被修改了。此时,运行 WriterDemo,就会发现价格不再发生变化了。
```
定价:Book{name='Web全栈开发进阶之路', price=79}
促销价:Book{name='Web全栈开发进阶之路', price=79}
```
### 04、总结
不可变类有很多优点,就像之前提到的 String 类那样,尤其是在多线程环境下,它非常的安全。尽管每次修改都会创建一个新的对象,增加了内存的消耗,但这个缺点相比它带来的优点,显然是微不足道的——无非就是捡了西瓜,丢了芝麻。
## 从原理上搞懂可变参数,就靠它了
为了让铁粉们能白票到阿里云的服务器,老王当了整整两天的客服,真正体验到了什么叫做“为人民群众谋福利”的不易和辛酸。正在他眼睛红肿打算要休息之际,小二跑过来问他:“Java 的可变参数究竟是怎么一回事?”老王一下子又清醒了,他爱 Java,他爱传道解惑,他爱这群尊敬他的读者。
*PS:star 这种事,只能求,不求没效果,铁子们,《Java 程序员进阶之路》在 GitHub 上已经收获了 514 枚星标,铁子们赶紧去点点了,冲 600 star*
>https://github.com/itwanger/toBeBetterJavaer
可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(`is-a`)的值作为参数。就像下面这样。
```java
public static void main(String[] args) {
print("沉");
print("沉", "默");
print("沉", "默", "王");
print("沉", "默", "王", "二");
}
public static void print(String... strs) {
for (String s : strs)
System.out.print(s);
System.out.println();
}
```
静态方法 `print()` 就使用了可变参数,所以 `print("沉")` 可以,`print("沉", "默")` 也可以,甚至 3 个、 4 个或者更多个字符串都可以作为参数传递给 `print()` 方法。
说到可变参数,我想起来阿里巴巴开发手册上有这样一条规约。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/varables-01.png)
意思就是尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位。既然坑位有限,只能在最后,那么可变参数就只能有一个(悠着点,悠着点)。如果可变参数不在最后一位,IDE 就会提示对应的错误,如下图所示。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/varables-02.png)
可变参数看起来就像是个语法糖,它背后究竟隐藏了什么呢?老王想要一探究竟,它在追求真理这条路上一直很执着。
其实也很简单。**当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法**
这就是为什么可以使用数组作为参数来调用带有可变参数的方法的根本原因。代码如下所示。
```java
public static void main(String[] args) {
print(new String[]{"沉"});
print(new String[]{"沉", "默"});
print(new String[]{"沉", "默", "王"});
print(new String[]{"沉", "默", "王", "二"});
}
public static void print(String... strs) {
for (String s : strs)
System.out.print(s);
System.out.println();
}
```
那如果方法的参数是一个数组,然后像使用可变参数那样去调用方法的时候,能行得通吗?
*留个思考题,大家也可以去试一试*
那一般什么时候使用可变参数呢?
可变参数,可变参数,顾名思义,当一个方法需要处理任意多个相同类型的对象时,就可以定义可变参数。Java 中有一个很好的例子,就是 String 类的 `format()` 方法,就像下面这样。
```java
System.out.println(String.format("年纪是: %d", 18));
System.out.println(String.format("年纪是: %d 名字是: %s", 18, "沉默王二"));
```
`%d` 表示将整数格式化为 10 进制整数,`%s` 表示输出字符串。
如果不使用可变参数,那需要格式化的参数就必须使用“+”号操作符拼接起来了。麻烦也就惹上身了。
在实际的项目代码中,开源包 slf4j.jar 的日志输出就经常要用到可变参数(log4j 就没法使用可变参数,日志中需要记录多个参数时就痛苦不堪了)。就像下面这样。
```java
protected Logger logger = LoggerFactory.getLogger(getClass());
logger.debug("名字是{}", mem.getName());
logger.debug("名字是{},年纪是{}", mem.getName(), mem.getAge());
```
查看源码就可以发现,`debug()` 方法使用了可变参数。
```java
public void debug(String format, Object... arguments);
```
那在使用可变参数的时候有什么注意事项吗?
有的。我们要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中。
```java
public static void main(String[] args) {
print(null);
}
public static void print(String... strs) {
for (String a : strs)
System.out.print(a);
System.out.println();
}
public static void print(Integer... ints) {
for (Integer i : ints)
System.out.print(i);
System.out.println();
}
```
这时候,编译器完全不知道该调用哪个 `print()` 方法,`print(String... strs)` 还是 `print(Integer... ints)`,傻傻分不清。
![](https://cdn.jsdelivr.net/gh/itwanger/toBeBetterJavaer/images/basic-extra-meal/varables-03.png)
假如真的需要重载带有可变参数的方法,就必须在调用方法的时候给出明确的指示,不要让编译器去猜。
```java
public static void main(String[] args) {
String [] strs = null;
print(strs);
Integer [] ints = null;
print(ints);
}
public static void print(String... strs) {
}
public static void print(Integer... ints) {
}
```
上面这段代码是可以编译通过的。因为编译器知道参数是 String 类型还是 Integer 类型,只不过为了运行时不抛出 `NullPointerException`,两个 `print()` 方法的内部要做好判空操作。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册