文章目录
- 1. compare比较接口
- 2. comparator又是啥?
- 3. 默认方法冲突
- 4. 四个内部类
- 4.1 普通内部类
- 4.2 局部内部类
- 4.3 匿名内部类
- 4.4 静态内部类
1. compare比较接口
这个接口其实用到的地方蛮多的,一般就是针对数组元素的排序,经常要用到,简单介绍下。
看如下代码,对我们自定义的Employee
对象数组进行正序排列。
public class Demo1 {
public static void main(String[] args) {
Employee e1 = new Employee(5, "c");
Employee e2 = new Employee(2, "b");
Employee e3 = new Employee(1, "a");
List<Employee> employees = new ArrayList<>(Arrays.asList(e1, e2, e3));
//使用工具类传入list集合
Collections.sort(employees);
System.out.println(employees);
//[Employee(id=1, name=a), Employee(id=2, name=b), Employee(id=5, name=c)]
}
}
@Data
@AllArgsConstructor
class Employee implements Comparable<Employee>{
private int id;
private String name;
@Override
public int compareTo(Employee other) {
return this.id - other.id;
}
}
其实排序很简单,需要我们实现Comparable
泛型填入自定义对象即可,对于compareTo
方法,我们是需要通过id来正序排列,所以当前对象的this.id
与other.id
进行比较,比如两个id分别是5,2,相减是正数,那么就需要调换位置,反之相减如果是负数,则不需要调换位置,排序就是根据两者的大小来比较的。
当然这个比较不是那么简单的,还有几点需要注意。
1.数据溢出的风险
两者相减不要超过int类型的范围,防止int溢出的情况,比较推荐的方法是Integer.compare(this.id, other.id);
这就不会有溢出的风险了。
2.保证对称性
对于两数的比较,若x.compareTo(y)
抛出异常,y.compareTo(x)
也同样要抛出异常,这点跟equals
类似。
可能有同学会问,这哪会出现这样的问题嘛,那我们来看下这个例子。
public class Demo1 {
public static void main(String[] args) {
Employee e1 = new Manager(5, "c");
//注意 这里将对象换成了Employee
Employee e2 = new Employee(2, "b");
Employee e3 = new Manager(1, "a");
List<Employee> employees = new ArrayList<>(Arrays.asList(e1, e2, e3));
//使用工具类传入list集合
Collections.sort(employees);
System.out.println(employees);
}
}
@Data
@AllArgsConstructor
class Employee implements Comparable<Employee>{
private int id;
private String name;
@Override
public int compareTo(Employee other) {
return Integer.compare(this.id, other.id);
}
}
class Manager extends Employee {
public Manager(int id, String name) {
super(id, name);
}
@Override
public int compareTo(Employee other) {
Manager manager = (Manager) other;//ClassCastException Employee不能被强转为Manager
return this.getId() - manager.getId();
}
}
这样排序就是有问题的,因为在父子类之间进行比较的话,Employee不能被强转为Manager,那就违反了上面的原则,补救办法有两个。
1.不同子类之间不能进行比较。
那就很简单,在compareTo
比较时,首先判断下类型是否一致即可。
@Override
public int compareTo(Employee other) {
if (getClass() != other.getClass()) {
throw new ClassCastException("类型不一致不能比较");
}
Manager manager = (Manager) other;
return this.getId() - manager.getId();
}
2.不同类之间可以比较
那就需要在父类里定义一个通用的比较方法,且定义成final,不允许重写,哪个子类都通过这个方法比较即可。
2. comparator又是啥?
明明有上面的比较接口就够了呀,为啥还要有这个接口?相信不少同学有这样的疑问,我之前也有,不过现在可以给大家解释,因为某些类已经定死了,比如String类,本身就是jdk自带的类,我们想让某些字符串按照特定规则排序,那原有的Compare
接口是做不到的,它只能做到让字符串根据字典顺序排列,但我们要是想让字符串数组按长度排列呢?
实现也很简单,我们只要重写Comparator#compare
,将字符串的长度进行比较,最终调用list.sort(Comparator<? super E> c)
排序即可。
public class Demo4 {
public static void main(String[] args) {
List<String> list = new ArrayList() {
{
add("cccc");
add("a");
add("bb");
}
};
LenComparator comparator = new LenComparator();
list.sort(comparator);
System.out.println(list);//[a, bb, cccc]
}
}
class LenComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();//按长度正序排列
}
}
这个比较器能够很大程度上丰富我们的排序需求,如无必要,勿增实体,大佬们也不太想要增加些新的类来实现一些已有的功能,只不过如果一个类不可变,不可再实现接口了,这个比较器就是一个好的选择。
3. 默认方法冲突
我们知道,在Java8里,为接口新增了default
与static
修饰符,但如果默认方法冲突了该怎么办?我们来看下列几种情况。
1.接口与类包含同名方法
看下面的代码就很容易知道了,以具体类中的方法为准,毕竟接口只是提供了某种规范。
public class Demo2 {
public static void main(String[] args) {
new Dad().run();//Dad run...
}
}
interface Person {
default void run() {
System.out.println("Person run...");
}
}
class Dad implements Person{
public void run() {
System.out.println("Dad run...");
}
}
2.两个接口有相同默认方法签名,且该类实现了两个接口
给出结论,需要我们强制覆盖对应的run
方法,自己决定最终的实现。
同理,如果一个接口是默认方法,一个是抽象方法,但签名相同(签名相同就是方法名和形参相同),也是需要我们自己重写对应的方法,解决二义性。
public class Demo2 {
public static void main(String[] args) {
new Dad().run();//Dad run...
}
}
interface Person {
default void run() {
System.out.println("Person run...");
}
}
interface People {
default void run() {
System.out.println("People run...");
}
}
//可以看到 两个接口有相同的方法签名 则需要强制覆盖
class Dad implements Person,People{
@Override
public void run() {
System.out.println("Dad run...");
}
}
3.某类继承了一个类,且实现了一个接口,但两者方法签名相同
给出结论,还是以类为准,父类其实已经对该方法做出了实现,子类可以选择是否重写,不重写编译器也不会报错。
public class Demo2 {
public static void main(String[] args) {
new Dad().run();
}
}
interface People {
default void run() {
System.out.println("People run...");
}
}
class Human {
public void run() {
System.out.println("Human run...");
}
}
class Dad extends Human implements People{
}
所以面对方法冲突的原则是,类优先,有冲突就自己解决。
4. 四个内部类
4.1 普通内部类
我们说下普通内部类的几个特点吧。
1.首先是权限修饰符,可以是private
或者是默认的,这取决于你是否需要外部访问该内部类。
2.内部类里可以直接访问外部类成员和方法,调用可以省略外部类.this
3.内部类的创建需要依赖外部类的对象,通过外部类对象.new 内部类()
的方式。
4.外部类不能直接访问内部类,只能通过内部类的对象来访问。
内部类的好处在于防止类重名,如果
class Outer {
private int id;
public void play() {
System.out.println("111");
}
public static void main(String[] args) {
Outer outer = new Outer();
//创建需要通过外部类的对象来创建
Outer.Inner inner = outer.new Inner();
inner.run();
}
private class Inner {
public void run() {
//可以访问外部类的成员和方法
System.out.println(id);//id等同于 Outer.this.id
Outer.this.play();//等同有 play()等同于 Outer.this.play()
}
}
}
4.2 局部内部类
与普通内部类不同的是,它存在于方法的内部,且class不能有任何修饰符,它的好处在于,对外部世界完全隐藏,当这个方法结束的时候,这个类也随之消亡了。
看示例,在方法内部通过创建内部类的对象完成调用,且能够直接访问run方法的形参。
public class Demo1 {
public static void main(String[] args) {
run("bb");
}
public static void run(String s) {
class Bb {
public void action() {
System.out.println(s);
}
}
//内部创建对象完成调用
new Bb().action();
}
}
4.3 匿名内部类
一般多用于抽象类或者接口的实例化,只要实现对应的抽象方法,就能完成实例化,但这个内部类是没有名称的,所以自然而然可以想到,它没有构造器。
需要注意的是,普通类对象的创建也可以在后面加括号,在类中重写原有的方法。所以匿名内部类功能很是强大。
public class Demo1 {
public static void main(String[] args) {
Listener listener = new Listener() {
@Override
public void run() {
System.out.println("监听启动~~");
}
};
//多数采用lambda的方式
Listener listener2 = () -> System.out.println("监听启动~~");
Hello hello = new Hello() {
@Override
void run() {
System.out.println("监听启动~~");
}
};
Dd dd = new Dd() {
@Override
public void run() {
System.out.println("run");
}
};
}
}
class Dd {
public void run() {
}
}
abstract class Hello {
abstract void run();
}
interface Listener {
void run();
}
4.4 静态内部类
静态内部类的创建可以不依赖与外部类对象,而是直接通过外部类的类名创建,它还可以任意访问外部类的静态或非静态成员和方法。
public class Demo6 {
public static void main(String[] args) {
//可以直接通过类名创建
Watch watch = new Demo6.Watch();
watch.say();
}
static final String WORDS = "hello bb";
static class Watch {
public void say() {
System.out.println(WORDS);
}
}
}
你很难注意到你拥有什么,除非它消失。