一:抽象类与接口入门
抽象类
由 abstract 关键字修饰的方法叫抽象方法;由 abstract 关键字修饰的类就是抽象类。
示例如下:
public abstract class Door {
private String name;
public Door(String name) {
this.name = name;
}
public abstract void open();
public abstract void close();
}
抽象类的特点:
抽象类不能被实例化
构造方法和 private/static 关键字修饰的方法不能是抽象的
如果父类是抽象类,子类必须实现父类的抽象方法,否则子类也要声明为一个抽象类
抽象类的优点:
强迫子类完成指定的行为
只提供模版,隐藏具体的细节,所有子类使用的都是相同的方法原型
抽象类是否一定要包含抽象方法?
当然不是。
抽象类可以只包含具体方法,没有抽象方法。
含有抽象方法的类一定是抽象类,但是抽象类不一定含有抽象方法。
那么,一个没有抽象方法的抽象类有什么意义呢?
首先,为什么要将这个类声明为抽象类?
抽象类最大的作用是不能被实例化,你需要一个类,它是一种抽象的存在,实例化这个类是没有意义的,它就可以被声明为抽象类;而不是说,我需要一个类中有某个抽象方法,才将这个类声明为一个抽象类。
所以,一个抽象类它可以没有抽象方法,而只包含具体方法,但是它是一种,不可被实例化,抽象的存在。抽象类作为父类可以定义一些字段,方法;在每个继承它的子类中重复声明这些字段,方法很麻烦,我们可以将其抽取出来放到公用的父类中。
所以没有抽象方法的抽象类的意义就是:
首先,这个类是一种抽象的存在,我不希望它被实例化。
简化代码,将公用的字段,方法抽取到父类中,方便子类使用。
接口
什么是接口?
Java 语言中,接口(interface)是一系列方法的声明,是一些方法特征的集合;一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。
示例如下:
public interface AlarmFunc {
void alarm();
}
我们为什么需要接口?
接口在一定程度上弥补了 Java 的单继承;子类只能继承一个父类,但是却可以实现多个接口,并且接口之间支持多继承。
接口就是一份契约,由实现接口的类来实现这个契约。在我们去定制类时,接口提供了灵活性与统一性。
接口可以包含什么?
若干个方法(默认 public)
若干成员变量
接口当中的成员变量被隐式声明为 public static final
接口可以 extends 接口,并且支持多继承,即:Interface A extends B,C
有方法体的默认方法(Since Java 8)
为什么 Java 中接口的成员变量被隐式声明为 public static final ?
- public
接口提供的是公共的模板或规范,为了让所有实现了该接口的类能够使用,就必须是 public 修饰的。
- static
static 作用于类定义,如果不用 static 声明,这就代表该成员是属于具体的对象,而不是这个类。
接口是一种定义了规范的“协议”,它不允许被实例化,所以如果接口内的成员变量不声明为 static 的,那么这个变量就毫无意义。而只有接口内的成员变量被声明为 static,才能说明该成员变量属于这个接口,才会让该变量具有存在的意义。
- final
如果一个变量被 final 修饰,那么就意味着它是不可变的。
我们反过来想一想,如果接口中的成员变量不被 final 修饰,那么就意味着它是可变的,那么也就意味着如果该成员变量被改变,所有实现了该接口的类都会受到影响,牵一发而动全身,这不就乱套了。
接口就是一种定义规范的“类”。如果随随便便来个类都可以修改规范,那么接口定义的还叫什么规范呢?所以,接口中的成员变量需要声明为 final。
接口当中的默认方法
在 Java 8 发布之前,接口中所有的方法都只能是 public abstract 修饰的抽象方法,但是从 Java 8 之后,接口当中便允许有实体的默认方法,这实际上是一种妥协的产物。
public interface Func {
void f1();
void f2();
default void f3(){
System.out.println("hello");
}
}
假如我使用的第三方接口,想要添加或修改代码,那么就会造成向后兼容性(backward compatibility)的问题,即:如果我使用的第三方接口添加了一个抽象方法,那么我实现了该接口的类就必须要实现这个新增的方法,否则就会报错。
为了避免这样的问题,Java 8 后,接口允许有实体的默认方法,这是 Java 妥协的产物,好处是接口的扩展性增强了,坏处是再次引入了 C++ 的二义性问题。
接口和抽象类的异同
抽象类和接口的相同点:
二者都无法被实例化
一个具体类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
抽象类和接口的不同点:
接口比抽象类要更加抽象;抽象类的本质就是一个类(除了无法实例化),既可以定义构造器,也可以有抽象方法和具体方法(抽象类甚至可以没有抽象方法只有具体方法);而接口中不能定义构造器并且其中的方法全部都是抽象方法。
抽象类中的成员变量可以是 private、default、protected、public 的,抽象类中的抽象方法可以是 default、protected、public 的;而接口中的方法默认全都是 public abstract 修饰的,接口中的成员变量则被隐式声明为 public static final
抽象类可以被继承(extends);接口则可以被继承(接口可以继承接口,且为多继承)和被实现类实现(implements)
什么时候使用抽象类,什么时候使用接口?
抽象类着重继承关系,接口则着重功能
举一个生动的例子:
你的需求是实现一个有报警功能的门
你可以定义一个抽象的 Door 类,具有 close() 与 open() 方法
而报警则是一个功能,和门的关系实则不大,不仅门可以有报警功能,车,保险柜都可以有报警功能;所以报警 alarm() 这个方法则可以定义在接口中
所以,你就可以定义一个抽象类 Door ,然后这个带报警功能的门 AlarmDoor 就可以继承 Door 这个抽象类并实现报警接口
接口:AlarmFunc
public interface AlarmFunc {
void alarm();
}
抽象类:Door
public abstract class Door {
public abstract void open();
public abstract void close();
}
具体实现类:AlarmDoor
public class AlarmDoor extends Door implements AlarmFunc{
@Override
public void alarm() {
System.out.println("alarm");
}
@Override
public void open() {
System.out.println("open the door");
}
@Override
public void close() {
System.out.println("close the door");
}
}
二:实现一个文件过滤器
目标:实现一个文件过滤器,通过给定文件夹路径以及定文件扩展名,过滤出所有该文件夹(及其后代子文件夹中)匹配指定扩展名的文件的名字。
譬如,我的文件根目录 test-root 的层级结构如下所示:
过滤出根目录为 test-root 路径下,所有文件扩展名为 “.csv” 的文件,返回结果为:
[3.csv, 2.csv, 7.csv, 4.csv]
代码如下:
MyFileFilterVisitor
package com.github.hcsp.polymorphism;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
public class MyFileFilterVisitor extends SimpleFileVisitor<Path> {
private String extension;
private List<String> filteredFileNames = new ArrayList<>();
public MyFileFilterVisitor(String extension) {
this.extension = extension;
}
public List<String> getFilteredFileNames() {
return filteredFileNames;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("Visit " + file);
if (file.toString().endsWith(extension)) {
filteredFileNames.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
}
FileFilter
package com.github.hcsp.polymorphism;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}
List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}
/**
* 实现一个按照扩展名过滤文件的功能
*
* @param rootDirectory 要过滤的文件夹
* @param extension 要过滤的文件扩展名,例如 .txt
* @return 所有该文件夹(及其后代子文件夹中)匹配指定扩展名的文件的名字
*/
public static List<String> filter(Path rootDirectory, String extension) throws IOException {
MyFileFilterVisitor visitor = new MyFileFilterVisitor(extension);
Files.walkFileTree(rootDirectory, visitor);
return visitor.getFilteredFileNames();
}
}
我们也可以将其简化为匿名内部类的形式
这样我们就可以省去 MyFileFilterVisitor 的代码
代码如下:
FileFilter
package com.github.hcsp.polymorphism;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}
List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}
/**
* 实现一个按照扩展名过滤文件的功能
*
* @param rootDirectory 要过滤的文件夹
* @param extension 要过滤的文件扩展名,例如 .txt
* @return 所有该文件夹(及其后代子文件夹中)匹配指定扩展名的文件的名字
*/
public static List<String> filter(Path rootDirectory, String extension) throws IOException {
List<String> res = new ArrayList<>();
Files.walkFileTree(rootDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("Visit file " + file);
if (file.toString().endsWith(extension)) {
res.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
});
return res;
}
}
三:Comparable 与 Comparator 接口
Comparable 接口
Comparable 翻译过来的含义是:可比较的
也就是说,如果一个类实现了 Comparable 接口,那么就意味着该类有既定的比较规则,支持排序,而不需要指定比较器。
我们常用的 Integer ,String 等一些类实现了 Comparable 接口,这也就意味着 Integer,String 都是可排序的。
实现 Comparable 接口的类的必须实现 int compareTo(T o) 方法
该类的对象可以通过:
x.compareTo(y)
来比较 x 和 y 的大小。
如果返回负数,则意味着 x 小于 y
如果返回零,则意味着 x 等于 y
- 如果返回正数,则意味着 x 大于 y
Comparator 接口
Comparator 翻译为比较器。如果说,我们需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable 接口);那么,我们可以通过“实现 Comparator 类来新建一个比较器”,人为地制定一个”比较规则“,然后通过该比较器对类进行排序。
实现 Comparator 接口必须实现 int compare(T o1,T o2) 方法,其原则和 Comparable 接口的 compareTo() 方法是一样的。
示例程序
示例1
有 Integer 类型的数组:
Integer[] arr = new Integer[]{3,4,5,10,2,1,6};
对其进行正序与倒序排序.
因为 Integer 实现了Comparable 接口,我们可以直接使用 Arrays.sort()
方法,Arrays.sort()
默认为从小到大排序
// 正序排序
Arrays.sort(arr);
// 如果想要实现倒序排序,我们需要重新定义比较器;或者,因为Integer实现了Comparable接口,我们可以直接使用Collections.reverseOrder()返回一个倒序的比较器
// 1. 倒序排序
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
// 2. 前面的程序可以使用 Lambda 表达式进行简化
Arrays.sort(arr, (o1, o2) -> o2 - o1);
// 3. 因为 Integer 实现了 Comparable 接口,我们可以直接使用 Collections.reverseOrder() 返回一个倒序的比较器
Arrays.sort(arr, Collections.reverseOrder());
示例2:
已知 Student 类:
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
Student s1 = new Student(3,"Jack");
Student s2 = new Student(7,"Rose");
Student s3 = new Student(1,"Jerry");
Student s4 = new Student(9,"Tom");
Student[] students = {s1,s2,s3,s4};
}
}
请对 students 数组按照 id 升序及降序进行排序
第一种方法:自定义比较器
import java.util.Arrays;
import java.util.Comparator;
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
// 按照学生id由小到小大
public static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.id - o2.id;
}
}
// 按照学生id由大到小
public static class IdDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.id - o1.id;
}
}
public static void main(String[] args) {
Student s1 = new Student(3, "Jack");
Student s2 = new Student(7, "Rose");
Student s3 = new Student(1, "Jerry");
Student s4 = new Student(9, "Tom");
Student[] students = {s1, s2, s3, s4};
// 升序排序
Arrays.sort(students, new IdAscendingComparator());
// 降序排序
Arrays.sort(students, new IdDescendingComparator());
}
}
第二种方法:
import java.util.Arrays;
import java.util.Comparator;
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
Student s1 = new Student(3, "Jack");
Student s2 = new Student(7, "Rose");
Student s3 = new Student(1, "Jerry");
Student s4 = new Student(9, "Tom");
Student[] students = {s1, s2, s3, s4};
// 升序排序
Arrays.sort(students, Comparator.comparingInt(o -> o.id));
// 降序排序
Arrays.sort(students, (o1, o2) -> o2.id - o1.id);
}
}
第三种方法:让 student 类实现 Comparable 接口,让其成为一个有比较规则的类
import java.util.Arrays;
import java.util.Collections;
public class Student implements Comparable<Student>{
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
Student s1 = new Student(3, "Jack");
Student s2 = new Student(7, "Rose");
Student s3 = new Student(1, "Jerry");
Student s4 = new Student(9, "Tom");
Student[] students = {s1, s2, s3, s4};
// 升序排序
Arrays.sort(students);
// 降序排序
Arrays.sort(students, Collections.reverseOrder());
}
@Override
public int compareTo(Student o) {
return this.id - o.id;
}
}
sort-by-multiple-fields
按照多重字段进行排序
代码如下:
package com.github.hcsp.polymorphism;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Point implements Comparable<Point> {
private final int x;
private final int y;
// 代表笛卡尔坐标系中的一个点
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Point point = (Point) o;
if (x != point.x) {
return false;
}
return y == point.y;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}
@Override
public String toString() {
return String.format("(%d,%d)", x, y);
}
// 按照先x再y,从小到大的顺序排序
// 例如排序后的结果应该是 (-1, 1) (1, -1) (2, -1) (2, 0) (2, 1)
public static List<Point> sort(List<Point> points) {
Collections.sort(points);
return points;
}
public static void main(String[] args) throws IOException {
List<Point> points =
Arrays.asList(
new Point(2, 0),
new Point(-1, 1),
new Point(1, -1),
new Point(2, 1),
new Point(2, -1));
System.out.println(Point.sort(points));
}
@Override
public int compareTo(Point o) {
if (this.getX() < o.getX()) {
return -1;
} else if (this.getX() > o.getX()) {
return 1;
} else {
if (this.getY() < o.getY()) {
return -1;
} else if (this.getY() > o.getY()) {
return 1;
} else {
return 0;
}
}
}
}
四:实现一个通用的过滤器
实现一个通用的过滤器代码;
package com.github.hcsp.polymorphism;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class User {
private static List<User> users;
private static Predicate<User> predicate;
/**
* 用户ID,数据库主键,全局唯一
*/
private final Integer id;
/**
* 用户名
*/
private final String name;
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
// 过滤ID为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
return filter(users, user -> user.id % 2 == 0);
}
// 过滤姓张的用户
public static List<User> filterZhangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("张"));
}
// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("王"));
}
// 你可以发现,在上面三个函数中包含大量的重复代码。
// 请尝试通过Predicate接口将上述代码抽取成一个公用的过滤器函数
// 并简化上面三个函数
public static List<User> filter(List<User> users, Predicate<User> predicate) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (predicate.test(user)) {
results.add(user);
}
}
return results;
}
}
五:内部类详解
什么是内部类
将一个类定义在一个类或者一个方法里面,这样的类就叫做内部类
内部类可以划分为四种:
成员内部类
静态内部类
匿名内部类
局部内部类
成员内部类
成员内部类是最普通的一种内部类,成员内部类可以访问外部类所有属性和方法。
但是,外部类如果要访问内部类的属性和方法,必须要先实例化成员内部类。
成员内部类中不能包含静态的属性和方法
public class Outer {
private int a = 1;
public void test1() {
System.out.println("Outer test1 method.");
}
class Inner {
private String b = "inner";
private void test2() {
System.out.println("Inner test2 method.");
}
// 内部类可以直接调用外部类的属性和方法
private void test() {
System.out.println(a);
test1();
}
}
// 外部类想要访问内部类的成员变量和方法,则需要先实例化内部类
public void test() {
Inner inner = new Inner();
System.out.println(inner.b);
inner.test2();
}
}
静态内部类
静态内部类就是在成员内部类前多加了一个 static 关键字。
静态内部类中是可以存在静态变量和静态方法的。
静态内部类只能访问外部类的静态成员变量和方法(包括私有静态)。
public class Outer {
private int a = 1;
public int getA() {
return a;
}
private static int b = 2;
public void test1() {
System.out.println("Outer method.");
}
public static void test2() {
System.out.println("Outer static method");
}
static class Inner {
private String c = "c";
private static String d = "d";
public void test3() {
System.out.println("Inner method.");
}
private static void test4() {
System.out.println("Inner static method.");
}
public void test() {
// 静态内部类可以直接访问外部类的所有静态资源
// 在静态内部类中如果想要访问外部类的成员变量和普通方法,需要先实例化外部类
Outer outer = new Outer();
outer.getA();
outer.test1();
System.out.println(b);
test2();
}
}
// 外部类可以直接通过 Inner.xxx 来访问内部类的静态资源
// 外部类如果想要访问内部类的成员变量和普通方法,则需要先实例化内部类
public void test() {
Inner inner = new Inner();
System.out.println(inner.c);
System.out.println(Inner.d);
inner.test3();
Inner.test4();
}
}
匿名内部类
在我们的实现一个通用的过滤器这一章节,我们的代码实际上已经使用匿名内部类了,只不过我将匿名内部类简化为了更为方便的 Lambda 表达式形式
使用 Lambda 表达式的代码:
// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("王"));
}
使用匿名内部类的代码:
// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.name.startsWith("王");
}
});
}
那么什么是匿名内部类呢?
匿名内部类也就是没有名字的类。
正因为没有名字,所以匿名内部类只能使用一次,通常用来简化代码的编写。
使用匿名内部类的前提就是存在一个类或者接口,且匿名内部类是写在方法中的。
在我们实际的开发中,匿名内部类的使用是相当频繁的,当我们看到抽象类或者接口作为参数,而且这个时候,我们需要的是一个对象;如果实现的方法仅仅调用一次,我们就可以使用匿名内部类来简化我们的代码。
如示例:
public class Test {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("do sth");
}
};
Thread t = new Thread(runnable);
t.start();
}
}
局部内部类
局部内部类就是定义在代码块内的一个内部类。
局部内部类的作用范围仅仅在它所在的代码块里。局部内部类不能被 public,protected,private 以及 static 修饰,但是可以被 final 修饰。
普通内部类(成员内部类)和静态内部类的区别
普通内部类:
可以访问外部类的所有属性和方法
成员内部类中不能包含静态的属性和方法
静态内部类:
静态内部类只能访问外部类的静态属性及方法,无法访问普通成员(变量和访问)
静态内部类可以包含静态的属性和方法
那么,为什么普通内部类可以访问到外部类的成员变量呢?
我们来看一下示例:
Home
package com.github.test;
public class Home {
class A {
}
}
Home2
package com.github.test;
public class Home2 {
static class A {
}
}
执行编译后,我们来到 target 目录下,来查看 Home 与 Home2 反编译的 class 文件
javap -private 'Home$A'
Home$A.class
class com.github.test.Home$A {
final com.github.test.Home this$0;
com.github.test.Home$A(com.github.test.Home);
}
javap -private 'Home2$A'
Home2$A.class
class com.github.test.Home2$A {
com.github.test.Home2$A();
}
我们可以看到 Home 类当中含有普通内部类 A,其反编译后的文件中有一个特殊的字段final com.github.test.Home this$0;
这个字段是 JDK 为我们自动添加的,指向外部类 Home 。
所以我们也就清楚了,之所以普通内部类可以直接访问外部类的成员变量是因为 JDK 为我们偷偷添加了一个 this$0,指向外部类。
什么时候使用内部类,什么时候使用静态内部类?
《Effective java》 第 24 条的内容是:Favor static member classes over nonstatic ,即:优先考虑使用静态内部类。
因为非静态内部类会持有外部类的一个隐式引用(this$0), 存储这个引用需要占用时间和空间。更严重的是有可能会导致宿主类在满足垃圾回收的条件时却仍然驻留在内存中,由此引发内存泄漏的问题。
所以,我们应该尽可能使用静态内部类。