美文网首页Java基础知识
怒肝俩月,新鲜出炉史上最有趣的Java小白手册,第一版(第二部分

怒肝俩月,新鲜出炉史上最有趣的Java小白手册,第一版(第二部分

作者: 沉默王二 | 来源:发表于2020-06-13 08:49 被阅读0次

    九、Java 抽象类

    当我们要完成的任务是确定的,但具体的方式需要随后开个会投票的话,Java 的抽象类就派上用场了。这句话怎么理解呢?搬个小板凳坐好,听我来给你讲讲。

    01、抽象类的 5 个关键点

    1)定义抽象类的时候需要用到关键字 abstract,放在 class 关键字前。

    public abstract class AbstractPlayer {
    }
    

    关于抽象类的命名,阿里出品的 Java 开发手册上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,记住了哦。

    2)抽象类不能被实例化,但可以有子类。

    尝试通过 new 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。

    通过 extends 关键字可以继承抽象类,继承后,BasketballPlayer 类就是 AbstractPlayer 的子类。

    public class BasketballPlayer extends AbstractPlayer {
    }
    

    3)如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。

    当在一个普通类(没有使用 abstract 关键字修饰)中定义了抽象方法,编译器就会有两处错误提示。

    第一处在类级别上,提醒你“这个类必须通过 abstract 关键字定义”,or 的那个信息没必要,见下图。

    第二处在方法级别上,提醒你“抽象方法所在的类不是抽象的”,见下图。

    4)抽象类可以同时声明抽象方法和具体方法,也可以什么方法都没有,但没必要。就像下面这样:

    public abstract class AbstractPlayer {
        abstract void play();
        
        public void sleep() {
            System.out.println("运动员也要休息而不是挑战极限");
        }
    }
    

    5)抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类中定义了 play() 方法,子类中就必须实现。

    public class BasketballPlayer extends AbstractPlayer {
        @Override
        void play() {
            System.out.println("我是张伯伦,篮球场上得过 100 分");
        }
    }
    

    如果没有实现的话,编译器会提醒你“子类必须实现抽象方法”,见下图。

    02、什么时候用抽象类

    与抽象类息息相关的还有一个概念,就是接口,我们留到下一篇文章中详细说,因为要说的知识点还是蛮多的。你现在只需要有这样一个概念就好,接口是对行为的抽象,抽象类是对整个类(包含成员变量和行为)进行抽象。

    (是不是有点明白又有点不明白,别着急,翘首以盼地等下一篇文章出炉吧)

    除了接口之外,还有一个概念就是具体的类,就是不通过 abstract 修饰的普通类,见下面这段代码中的定义。

    public class BasketballPlayer {
       public void play() {
            System.out.println("我是詹姆斯,现役第一人");
        }
    }
    

    有接口,有具体类,那什么时候该使用抽象类呢?

    1)我们希望一些通用的功能被多个子类复用。比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep(),表明所有运动员都需要休息,那么这个方法就可以被子类复用。

    public abstract class AbstractPlayer {
        public void sleep() {
            System.out.println("运动员也要休息而不是挑战极限");
        }
    }
    

    虽然 AbstractPlayer 类可以不是抽象类——把 abstract 修饰符去掉也能满足这种场景。但 AbstractPlayer 类可能还会有一个或者多个抽象方法。

    BasketballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。

    public class BasketballPlayer extends AbstractPlayer {
    }
    

    BasketballPlayer 对象可以直接调用 sleep() 方法:

    BasketballPlayer basketballPlayer = new BasketballPlayer();
    basketballPlayer.sleep();
    

    FootballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。

    public class FootballPlayer extends AbstractPlayer {
    }
    

    FootballPlayer 对象也可以直接调用 sleep() 方法:

    FootballPlayer footballPlayer = new FootballPlayer();
    footballPlayer.sleep();
    

    2)我们需要在抽象类中定义好 API,然后在子类中扩展实现。比如说,AbstractPlayer 抽象类中有一个抽象方法 play(),定义所有运动员都可以从事某项运动,但需要对应子类去扩展实现。

    public abstract class AbstractPlayer {
        abstract void play();
    }
    

    BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

    public class BasketballPlayer extends AbstractPlayer {
        @Override
        void play() {
            System.out.println("我是张伯伦,我篮球场上得过 100 分,");
        }
    }
    

    FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

    public class FootballPlayer extends AbstractPlayer {
        @Override
        void play() {
            System.out.println("我是C罗,我能接住任意高度的头球");
        }
    }
    

    3)如果父类与子类之间的关系符合 is-a 的层次关系,就可以使用抽象类,比如说篮球运动员是运动员,足球运动员是运动员。

    03、具体示例

    为了进一步展示抽象类的特性,我们再来看一个具体的示例。假设现在有一个文件,里面的内容非常简单——“Hello World”,现在需要有一个读取器将内容读取出来,最好能按照大写的方式,或者小写的方式。

    这时候,最好定义一个抽象类,比如说 BaseFileReader:

    public abstract class BaseFileReader {
        protected Path filePath;
    
        protected BaseFileReader(Path filePath) {
            this.filePath = filePath;
        }
    
        public List<String> readFile() throws IOException {
            return Files.lines(filePath)
                    .map(this::mapFileLine).collect(Collectors.toList());
        }
    
        protected abstract String mapFileLine(String line);
    }
    

    filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问。

    readFile() 方法用来读取文件,方法体里面调用了抽象方法 mapFileLine()——需要子类扩展实现大小写的方式。

    你看,BaseFileReader 设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。

    小写的方式:

    public class LowercaseFileReader extends BaseFileReader {
        protected LowercaseFileReader(Path filePath) {
            super(filePath);
        }
    
        @Override
        protected String mapFileLine(String line) {
            return line.toLowerCase();
        }
    }
    

    大写的方式:

    public class UppercaseFileReader extends BaseFileReader {
        protected UppercaseFileReader(Path filePath) {
            super(filePath);
        }
    
        @Override
        protected String mapFileLine(String line) {
            return line.toUpperCase();
        }
    }
    

    你看,从文件里面一行一行读取内容的代码被子类复用了——抽象类 BaseFileReader 类中定义的普通方法 readFile()。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。

    接下来,我们来新建一个测试类 FileReaderTest:

    public class FileReaderTest {
        public static void main(String[] args) throws URISyntaxException, IOException {
            URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");
            Path path = Paths.get(location.toURI());
            BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
            BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);
            System.out.println(lowercaseFileReader.readFile());
            System.out.println(uppercaseFileReader.readFile());
        }
    }
    

    项目的 resource 目录下有一个文本文件,名字叫 helloworld.txt。

    可以通过 ClassLoader.getResource() 的方式获取到该文件的 URI 路径,然后就可以使用 LowercaseFileReader 和 UppercaseFileReader 两种方式读取到文本内容了。

    输出结果如下所示:

    [hello world]
    [HELLO WORLD]
    

    十、Java 接口

    对于面向对象编程来说,抽象是一个极具魅力的特征。如果一个程序员的抽象思维很差,那他在编程中就会遇到很多困难,无法把业务变成具体的代码。在 Java 中,可以通过两种形式来达到抽象的目的,一种是抽象类,另外一种就是接口。

    如果你现在就想知道抽象类与接口之间的区别,我可以提前给你说一个:

    • 一个类只能继承一个抽象类,但却可以实现多个接口。

    当然了,在没有搞清楚接口到底是什么,它可以做什么之前,这个区别理解起来会有点难度。

    01、接口是什么

    接口是通过 interface 关键字定义的,它可以包含一些常量和方法,来看下面这个示例。

    public interface Electronic {
        // 常量
        String LED = "LED";
    
        // 抽象方法
        int getElectricityUse();
    
        // 静态方法
        static boolean isEnergyEfficient(String electtronicType) {
            return electtronicType.equals(LED);
        }
    
        // 默认方法
        default void printDescription() {
            System.out.println("电子");
        }
    }
    

    1)接口中定义的变量会在编译的时候自动加上 public static final 修饰符,也就是说 LED 变量其实是一个常量。

    Java 官方文档上有这样的声明:

    Every field declaration in the body of an interface is implicitly public, static, and final.

    换句话说,接口可以用来作为常量类使用,还能省略掉 public static final,看似不错的一种选择,对吧?

    不过,这种选择并不可取。因为接口的本意是对方法进行抽象,而常量接口会对子类中的变量造成命名空间上的“污染”。

    2)没有使用 privatedefault 或者 static 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract 修饰符。也就是说 getElectricityUse() 其实是一个抽象方法,没有方法体——这是定义接口的本意。

    3)从 Java 8 开始,接口中允许有静态方法,比如说 isEnergyEfficient() 方法。

    静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口的名字来调用,比如说 Electronic.isEnergyEfficient("LED")

    接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。

    4)接口中允许定义 default 方法也是从 Java 8 开始的,比如说 printDescription(),它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现,也就是说,无法直接使用一个“;”号来结束默认方法——编译器会报错的。

    允许在接口中定义默认方法的理由是很充分的,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有 default 方法的帮助下,我们就必须挨个对实现类进行修改。

    来看一下 Electronic 接口反编译后的字节码吧,你会发现,接口中定义的所有变量或者方法,都会自动添加上 public 关键字——假如你想知道编译器在背后都默默做了哪些辅助,记住反编译字节码就对了。

    public interface Electronic
    {
    
        public abstract int getElectricityUse();
    
        public static boolean isEnergyEfficient(String electtronicType)
        {
            return electtronicType.equals("LED");
        }
    
        public void printDescription()
        {
            System.out.println("\u7535\u5B50");
        }
    
        public static final String LED = "LED";
    }
    

    有些读者可能会问,“二哥,为什么我反编译后的字节码和你的不一样,你用了什么反编译工具?”其实没有什么秘密,微信搜「沉默王二」回复关键字「JAD」就可以免费获取了,超级好用。

    02、定义接口的注意事项

    由之前的例子我们就可以得出下面这些结论:

    • 接口中允许定义变量
    • 接口中允许定义抽象方法
    • 接口中允许定义静态方法(Java 8 之后)
    • 接口中允许定义默认方法(Java 8 之后)

    除此之外,我们还应该知道:

    1)接口不允许直接实例化。

    需要定义一个类去实现接口,然后再实例化。

    public class Computer implements Electronic {
    
        public static void main(String[] args) {
            new Computer();
        }
    
        @Override
        public int getElectricityUse() {
            return 0;
        }
    }
    

    2)接口可以是空的,既不定义变量,也不定义方法。

    public interface Serializable {
    }
    

    Serializable 是最典型的一个空的接口,我之前分享过一篇文章《Java Serializable:明明就一个空的接口嘛》,感兴趣的读者可以去我的个人博客看一看,你就明白了空接口的意义。

    http://www.itwanger.com/java/2019/11/14/java-serializable.html

    3)不要在定义接口的时候使用 final 关键字,否则会报编译错误,因为接口就是为了让子类实现的,而 final 阻止了这种行为。

    4)接口的抽象方法不能是 private、protected 或者 final。

    5)接口的变量是隐式 public static final,所以其值无法改变。

    03、接口可以做什么

    1)使某些实现类具有我们想要的功能,比如说,实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。

    Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 Object.clone() 方法,否则会抛出 CloneNotSupportedException。

    public class CloneableTest implements Cloneable {
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    
        public static void main(String[] args) throws CloneNotSupportedException {
            CloneableTest c1 = new CloneableTest();
            CloneableTest c2 = (CloneableTest) c1.clone();
        }
    }
    

    运行后没有报错。现在把 implements Cloneable 去掉。

    public class CloneableTest {
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    
        public static void main(String[] args) throws CloneNotSupportedException {
            CloneableTest c1 = new CloneableTest();
            CloneableTest c2 = (CloneableTest) c1.clone();
    
        }
    }
    

    运行后抛出 CloneNotSupportedException:

    Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest
        at java.base/java.lang.Object.clone(Native Method)
        at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)
        at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)
    

    至于 Comparable 和 Comparator 的用法,感兴趣的读者可以参照我之前写的另外一篇文章《来吧,一文彻底搞懂Java中的Comparable和Comparator》。

    http://www.itwanger.com/java/2020/01/04/java-comparable-comparator.html

    2)Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。

    可能有些读者会问,“二哥,为什么 Java 只支持单一继承?”简单来解释一下。

    如果有两个类共同继承(extends)一个有特定方法的父类,那么该方法会被两个子类重写。然后,如果你决定同时继承这两个子类,那么在你调用该重写方法时,编译器不能识别你要调用哪个子类的方法。这也正是著名的菱形问题,见下图。

    ClassC 同时继承了 ClassA 和 ClassB,ClassC 的对象在调用 ClassA 和 ClassB 中重载的方法时,就不知道该调用 ClassA 的方法,还是 ClassB 的方法。

    接口没有这方面的困扰。来定义两个接口,Fly 会飞,Run 会跑。

    public interface Fly {
        void fly();
    }
    public interface Run {
        void run();
    }
    

    然后让一个类同时实现这两个接口。

    public class Pig implements Fly,Run{
        @Override
        public void fly() {
            System.out.println("会飞的猪");
        }
    
        @Override
        public void run() {
            System.out.println("会跑的猪");
        }
    }
    

    这就在某种形式上达到了多重继承的目的:现实世界里,猪的确只会跑,但在雷军的眼里,站在风口的猪就会飞,这就需要赋予这只猪更多的能力,通过抽象类是无法实现的,只能通过接口。

    3)实现多态。

    什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果,鼠标左键点击窗口上的 X 号可以关闭窗口,点击超链接却可以打开新的网页。

    多态可以通过继承(extends)的关系实现,也可以通过接口的形式实现。来看这样一个例子。

    Shape 是表示一个形状。

    public interface Shape {
        String name();
    }
    

    圆是一个形状。

    public class Circle implements Shape {
        @Override
        public String name() {
            return "圆";
        }
    }
    

    正方形也是一个形状。

    public class Square implements Shape {
        @Override
        public String name() {
            return "正方形";
        }
    }
    

    然后来看测试类。

    List<Shape> shapes = new ArrayList<>();
    Shape circleShape = new Circle();
    Shape squareShape = new Square();
    
    shapes.add(circleShape);
    shapes.add(squareShape);
    
    for (Shape shape : shapes) {
        System.out.println(shape.name());
    }
    

    多态的存在 3 个前提:

    1、要有继承关系,Circle 和 Square 都实现了 Shape 接口
    2、子类要重写父类的方法,Circle 和 Square 都重写了 name() 方法
    3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。

    然后,我们来看一下测试结果:

    圆
    正方形
    

    也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 name() 方法的时候,它知道 Circle 对象应该调用 Circle 类的 name() 方法,Square 对象应该调用 Square 类的 name() 方法。

    04、接口与抽象类的区别

    好了,关于接口的一切,你应该都搞清楚了。现在回到读者春夏秋冬的那条留言,“兄弟,说说抽象类和接口之间的区别?”

    1)语法层面上

    • 接口中不能有 public 和 protected 修饰的方法,抽象类中可以有。
    • 接口中的变量只能是隐式的常量,抽象类中可以有任意类型的变量。
    • 一个类只能继承一个抽象类,但却可以实现多个接口。

    2)设计层面上

    抽象类是对类的一种抽象,继承抽象类的类和抽象类本身是一种 is-a 的关系。

    接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系,所有的类都可以实现 Serializable 接口,从而具有序列化的功能。

    就这么多吧,能说道这份上,我相信面试官就不会为难你了。

    十一、Java 继承

    在 Java 中,一个类可以继承另外一个类或者实现多个接口,我想这一点,大部分的读者应该都知道了。还有一点,我不确定大家是否知道,就是一个接口也可以继承另外一个接口,就像下面这样:

    public interface OneInterface extends Cloneable {
    }
    

    这样做有什么好处呢?我想有一部分读者应该已经猜出来了,就是实现了 OneInterface 接口的类,也可以使用 Object.clone() 方法了。

    public class TestInterface implements OneInterface {
        public static void main(String[] args) throws CloneNotSupportedException {
            TestInterface c1 = new TestInterface();
            TestInterface c2 = (TestInterface) c1.clone();
        }
    }
    

    除此之外,我们还可以在 OneInterface 接口中定义其他一些抽象方法(比如说深拷贝),使该接口拥有 Cloneable 所不具有的功能。

    public interface OneInterface extends Cloneable {
        void deepClone();
    }
    

    看到了吧?这就是继承的好处:子接口拥有了父接口的方法,使得子接口具有了父接口相同的行为;同时,子接口还可以在此基础上自由发挥,添加属于自己的行为

    以上,把“接口”换成“类”,结论同样成立。让我们来定义一个普通的父类 Wanger:

    public class Wanger {
        int age;
        String name;
        void write() {
            System.out.println("我写了本《基督山伯爵》");
        }
    }
    

    然后,我们再来定义一个子类 Wangxiaoer,使用关键字 extends 来继承父类 Wanger:

    public class Wangxiaoer extends Wanger{
        @Override
        void write() {
            System.out.println("我写了本《茶花女》");
        }
    }
    

    我们可以将通用的方法和成员变量放在父类中,达到代码复用的目的;然后将特殊的方法和成员变量放在子类中,除此之外,子类还可以覆盖父类的方法(比如write() 方法)。这样,子类也就焕发出了新的生命力。

    Java 只支持单一继承,这一点,我在上一篇接口的文章中已经提到过了。如果一个类在定义的时候没有使用 extends 关键字,那么它隐式地继承了 java.lang.Object 类——在我看来,这恐怕就是 Java 号称万物皆对象的真正原因了。

    那究竟子类继承了父类的什么呢?

    子类可以继承父类的非 private 成员变量,为了验证这一点,我们来看下面这个示例。

    public class Wanger {
        String defaultName;
        private String privateName;
        public String publicName;
        protected String protectedName;
    }
    

    父类 Wanger 定义了四种类型的成员变量,缺省的 defaultName、私有的 privateName、共有的 publicName、受保护的 protectedName。

    在子类 Wangxiaoer 中定义一个测试方法 testVariable()

    可以确认,除了私有的 privateName,其他三种类型的成员变量都可以继承到。

    同理,子类可以继承父类的非 private 方法,为了验证这一点,我们来看下面这个示例。

    public class Wanger {
        void write() {
        }
    
        private void privateWrite() {
        }
    
        public void publicWrite() {
        }
    
        protected void protectedWrite() {
        }
    }
    

    父类 Wanger 定义了四种类型的方法,缺省的 write、私有的 privateWrite()、共有的 publicWrite()、受保护的 protectedWrite()。

    在子类 Wangxiaoer 中定义一个 main 方法,并使用 new 关键字新建一个子类对象:

    可以确认,除了私有的 privateWrite(),其他三种类型的方法都可以继承到。

    不过,子类无法继承父类的构造方法。如果父类的构造方法是带有参数的,代码如下所示:

    public class Wanger {
        int age;
        String name;
    
        public Wanger(int age, String name) {
            this.age = age;
            this.name = name;
        }
    }
    

    则必须在子类的构造器中显式地通过 super 关键字进行调用,否则编译器将提示以下错误:

    修复后的代码如下所示:

    public class Wangxiaoer extends Wanger{
        public Wangxiaoer(int age, String name) {
            super(age, name);
        }
    }
    

    is-a 是继承的一个明显特征,就是说子类的对象引用类型可以是一个父类类型。

    public class Wangxiaoer extends Wanger{
        public static void main(String[] args) {
            Wanger wangxiaoer = new Wangxiaoer();
        }
    }
    

    同理,子接口的实现类的对象引用类型也可以是一个父接口类型。

    public interface OneInterface extends Cloneable {
    }
    public class TestInterface implements OneInterface {
        public static void main(String[] args) {
            Cloneable c1 = new TestInterface();
        }
    }
    

    尽管一个类只能继承一个类,但一个类却可以实现多个接口,这一点,我在上一篇文章也提到过了。另外,还有一点我也提到了,就是 Java 8 之后,接口中可以定义 default 方法,这很方便,但也带来了新的问题:

    如果一个类实现了多个接口,而这些接口中定义了相同签名的 default 方法,那么这个类就要重写该方法,否则编译无法通过。

    FlyInterface 是一个会飞的接口,里面有一个签名为 sleep() 的默认方法:

    public interface FlyInterface {
        void fly();
        default void sleep() {
            System.out.println("睡着飞");
        }
    }
    

    RunInterface 是一个会跑的接口,里面也有一个签名为 sleep() 的默认方法:

    public interface RunInterface {
        void run();
        default void sleep() {
            System.out.println("睡着跑");
        }
    }
    

    Pig 类实现了 FlyInterface 和 RunInterface 两个接口,但这时候编译出错了。

    原本,default 方法就是为实现该接口而不覆盖该方法的类提供默认实现的,现在,相同方法签名的 sleep() 方法把编译器搞懵逼了,只能重写了。

    public class Pig implements FlyInterface, RunInterface {
    
        @Override
        public void fly() {
            System.out.println("会飞的猪");
        }
    
        @Override
        public void sleep() {
            System.out.println("只能重写了");
        }
    
        @Override
        public void run() {
            System.out.println("会跑的猪");
        }
    }
    

    类虽然不能继承多个类,但接口却可以继承多个接口,这一点,我不知道有没有触及到一些读者的知识盲区。

    public interface WalkInterface extends FlyInterface,RunInterface{
        void walk();
    }
    

    十二、this 关键字

    在 Java 中,this 关键字指的是当前对象(它的方法正在被调用)的引用,能理解吧,各位亲?不理解的话,我们继续往下看。

    看完再不明白,你过来捶爆我,我保证不还手,只要不打脸。

    01、消除字段歧义

    我敢赌一毛钱,所有的读者,不管男女老少,应该都知道这种用法,毕竟写构造方法的时候经常用啊。谁要不知道,过来,我给你发一毛钱红包,只要你脸皮够厚。

    public class Writer {
        private int age;
        private String name;
    
        public Writer(int age, String name) {
            this.age = age;
            this.name = name;
        }
    }
    

    Writer 类有两个成员变量,分别是 age 和 name,在使用有参构造函数的时候,如果参数名和成员变量的名字相同,就需要使用 this 关键字消除歧义:this.age 是指成员变量,age 是指构造方法的参数。

    02、引用类的其他构造方法

    当一个类的构造方法有多个,并且它们之间有交集的话,就可以使用 this 关键字来调用不同的构造方法,从而减少代码量。

    比如说,在无参构造方法中调用有参构造方法:

    public class Writer {
        private int age;
        private String name;
    
        public Writer(int age, String name) {
            this.age = age;
            this.name = name;
        }
    
        public Writer() {
            this(18, "沉默王二");
        }
    }
    

    也可以在有参构造方法中调用无参构造方法:

    public class Writer {
        private int age;
        private String name;
    
        public Writer(int age, String name) {
            this();
            this.age = age;
            this.name = name;
        }
    
        public Writer() {
        }
    }
    

    需要注意的是,this() 必须是构造方法中的第一条语句,否则就会报错。

    03、作为参数传递

    在下例中,有一个无参的构造方法,里面调用了 print() 方法,参数只有一个 this 关键字。

    public class ThisTest {
        public ThisTest() {
            print(this);
        }
    
        private void print(ThisTest thisTest) {
            System.out.println("print " +thisTest);
        }
    
        public static void main(String[] args) {
            ThisTest test = new ThisTest();
            System.out.println("main " + test);
        }
    }
    

    来打印看一下结果:

    print com.cmower.baeldung.this1.ThisTest@573fd745
    main com.cmower.baeldung.this1.ThisTest@573fd745
    

    从结果中可以看得出来,this 就是我们在 main() 方法中使用 new 关键字创建的 ThisTest 对象。

    04、链式调用

    学过 JavaScript,或者 jQuery 的读者可能对链式调用比较熟悉,类似于 a.b().c().d(),仿佛能无穷无尽调用下去。

    在 Java 中,对应的专有名词叫 Builder 模式,来看一个示例。

    public class Writer {
        private int age;
        private String name;
        private String bookName;
        
        public Writer(WriterBuilder builder) {
            this.age = builder.age;
            this.name = builder.name;
            this.bookName = builder.bookName;
        }
    
        public static class WriterBuilder {
            public String bookName;
            private int age;
            private String name;
    
            public WriterBuilder(int age, String name) {
                this.age = age;
                this.name = name;
            }
    
            public WriterBuilder writeBook(String bookName) {
                this.bookName = bookName;
                return this;
            }
    
            public Writer build() {
                return new Writer(this);
            }
        }
    }
    

    Writer 类有三个成员变量,分别是 age、name 和 bookName,还有它们仨对应的一个构造方法,参数是一个内部静态类 WriterBuilder。

    内部类 WriterBuilder 也有三个成员变量,和 Writer 类一致,不同的是,WriterBuilder 类的构造方法里面只有 age 和 name 赋值了,另外一个成员变量 bookName 通过单独的方法 writeBook() 来赋值,注意,该方法的返回类型是 WriterBuilder,最后使用 return 返回了 this 关键字。

    最后的 build() 方法用来创建一个 Writer 对象,参数为 this 关键字,也就是当前的 WriterBuilder 对象。

    这时候,创建 Writer 对象就可以通过链式调用的方式。

    Writer writer = new Writer.WriterBuilder(18,"沉默王二")
                    .writeBook("《Web全栈开发进阶之路》")
                    .build();
    

    05、在内部类中访问外部类对象

    说实话,自从 Java 8 的函数式编程出现后,就很少用到 this 在内部类中访问外部类对象了。来看一个示例:

    public class ThisInnerTest {
        private String name;
        
        class InnerClass {
            public InnerClass() {
                ThisInnerTest thisInnerTest = ThisInnerTest.this;
                String outerName = thisInnerTest.name;
            }
        }
    }
    

    在内部类 InnerClass 的构造方法中,通过外部类.this 可以获取到外部类对象,然后就可以使用外部类的成员变量了,比如说 name。

    十三、super 关键字

    简而言之,super 关键字就是用来访问父类的。

    先来看父类:

    public class SuperBase {
        String message = "父类";
    
        public SuperBase(String message) {
            this.message = message;
        }
    
        public SuperBase() {
        }
    
        public void printMessage() {
            System.out.println(message);
        }
    }
    

    再来看子类:

    public class SuperSub extends SuperBase {
        String message = "子类";
    
        public SuperSub(String message) {
            super(message);
        }
    
        public SuperSub() {
            super.printMessage();
            printMessage();
        }
    
        public void getParentMessage() {
            System.out.println(super.message);
        }
    
        public void printMessage() {
            System.out.println(message);
        }
    }
    

    1)super 关键字可用于访问父类的构造方法

    你看,子类可以通过 super(message) 来调用父类的构造方法。现在来新建一个 SuperSub 对象,看看输出结果是什么:

    SuperSub superSub = new SuperSub("子类的message");
    

    new 关键字在调用构造方法创建子类对象的时候,会通过 super 关键字初始化父类的 message,所以此此时父类的 message 会输出“子类的message”。

    2)super 关键字可以访问父类的变量

    上述例子中的 SuperSub 类中就有,getParentMessage() 通过 super.message 方法父类的同名成员变量 message。

    3)当方法发生重写时,super 关键字可以访问父类的同名方法

    上述例子中的 SuperSub 类中就有,无参的构造方法 SuperSub() 中就使用 super.printMessage() 调用了父类的同名方法。

    十四、重写和重载

    先来看一段重写的代码吧。

    class LaoWang{
        public void write() {
            System.out.println("老王写了一本《基督山伯爵》");
        }
    }
    public class XiaoWang extends LaoWang {
        @Override
        public void write() {
            System.out.println("小王写了一本《茶花女》");
        }
    }
    

    重写的两个方法名相同,方法参数的个数也相同;不过一个方法在父类中,另外一个在子类中。就好像父类 LaoWang 有一个 write() 方法(无参),方法体是写一本《基督山伯爵》;子类 XiaoWang 重写了父类的 write() 方法(无参),但方法体是写一本《茶花女》。

    来写一段测试代码。

    public class OverridingTest {
        public static void main(String[] args) {
            LaoWang wang = new XiaoWang();
            wang.write();
        }
    }
    

    大家猜结果是什么?

    小王写了一本《茶花女》
    

    在上面的代码中,们声明了一个类型为 LaoWang 的变量 wang。在编译期间,编译器会检查 LaoWang 类是否包含了 write() 方法,发现 LaoWang 类有,于是编译通过。在运行期间,new 了一个 XiaoWang 对象,并将其赋值给 wang,此时 Java 虚拟机知道 wang 引用的是 XiaoWang 对象,所以调用的是子类 XiaoWang 中的 write() 方法而不是父类 LaoWang 中的 write() 方法,因此输出结果为“小王写了一本《茶花女》”。

    再来看一段重载的代码吧。

    class LaoWang{
        public void read() {
            System.out.println("老王读了一本《Web全栈开发进阶之路》");
        }
        
        public void read(String bookname) {
            System.out.println("老王读了一本《" + bookname + "》");
        }
    }
    

    重载的两个方法名相同,但方法参数的个数不同,另外也不涉及到继承,两个方法在同一个类中。就好像类 LaoWang 有两个方法,名字都是 read(),但一个有参数(书名),另外一个没有(只能读写死的一本书)。

    来写一段测试代码。

    public class OverloadingTest {
        public static void main(String[] args) {
            LaoWang wang = new LaoWang();
            wang.read();
            wang.read("金瓶梅");
        }
    }
    

    这结果就不用猜了。变量 wang 的类型为 LaoWang,wang.read() 调用的是无参的 read() 方法,因此先输出“老王读了一本《Web全栈开发进阶之路》”;wang.read("金瓶梅") 调用的是有参的 read(bookname) 方法,因此后输出“老王读了一本《金瓶梅》”。在编译期间,编译器就知道这两个 read() 方法时不同的,因为它们的方法签名(=方法名称+方法参数)不同。

    简单的来总结一下:

    1)编译器无法决定调用哪个重写的方法,因为只从变量的类型上是无法做出判断的,要在运行时才能决定;但编译器可以明确地知道该调用哪个重载的方法,因为引用类型是确定的,参数个数决定了该调用哪个方法。

    2)多态针对的是重写,而不是重载。

    哎,后悔啊,早年我要是能把这道面试题吃透的话,也不用被老马刁难了。吟一首诗感慨一下人生吧。

    青青园中葵,朝露待日晞。
    阳春布德泽,万物生光辉。
    常恐秋节至,焜黄华叶衰。
    百川东到海,何时复西归?
    少壮不努力,老大徒伤悲

    另外,我想要告诉大家的是,重写(Override)和重载(Overload)是 Java 中两个非常重要的概念,新手经常会被它们俩迷惑,因为它们俩的英文名字太像了,中文翻译也只差一个字。难,太难了。

    十五、static 关键字

    先来个提纲挈领(唉呀妈呀,成语区博主上线了)吧:

    static 关键字可用于变量、方法、代码块和内部类,表示某个特定的成员只属于某个类本身,而不是该类的某个对象。

    01、静态变量

    静态变量也叫类变量,它属于一个类,而不是这个类的对象。

    public class Writer {
        private String name;
        private int age;
        public static int countOfWriters;
    
        public Writer(String name, int age) {
            this.name = name;
            this.age = age;
            countOfWriters++;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    

    其中,countOfWriters 被称为静态变量,它有别于 name 和 age 这两个成员变量,因为它前面多了一个修饰符 static

    这意味着无论这个类被初始化多少次,静态变量的值都会在所有类的对象中共享。

    Writer w1 = new Writer("沉默王二",18);
    Writer w2 = new Writer("沉默王三",16);
    
    System.out.println(Writer.countOfWriters);
    

    按照上面的逻辑,你应该能推理得出,countOfWriters 的值此时应该为 2 而不是 1。从内存的角度来看,静态变量将会存储在 Java 虚拟机中一个名叫“Metaspace”(元空间,Java 8 之后)的特定池中。

    静态变量和成员变量有着很大的不同,成员变量的值属于某个对象,不同的对象之间,值是不共享的;但静态变量不是的,它可以用来统计对象的数量,因为它是共享的。就像上面例子中的 countOfWriters,创建一个对象的时候,它的值为 1,创建两个对象的时候,它的值就为 2。

    简单小结一下:

    1)由于静态变量属于一个类,所以不要通过对象引用来访问,而应该直接通过类名来访问;

    2)不需要初始化类就可以访问静态变量。

    public class WriterDemo {
        public static void main(String[] args) {
            System.out.println(Writer.countOfWriters); // 输出 0
        }
    }
    

    02、静态方法

    静态方法也叫类方法,它和静态变量类似,属于一个类,而不是这个类的对象。

    public static void setCountOfWriters(int countOfWriters) {
        Writer.countOfWriters = countOfWriters;
    }
    

    setCountOfWriters() 就是一个静态方法,它由 static 关键字修饰。

    如果你用过 java.lang.Math 类或者 Apache 的一些工具类(比如说 StringUtils)的话,对静态方法一定不会感动陌生。

    Math 类的几乎所有方法都是静态的,可以直接通过类名来调用,不需要创建类的对象。

    简单小结一下:

    1)Java 中的静态方法在编译时解析,因为静态方法不能被重写(方法重写发生在运行时阶段,为了多态)。

    2)抽象方法不能是静态的。

    3)静态方法不能使用 this 和 super 关键字。

    4)成员方法可以直接访问其他成员方法和成员变量。

    5)成员方法也可以直接方法静态方法和静态变量。

    6)静态方法可以访问所有其他静态方法和静态变量。

    7)静态方法无法直接访问成员方法和成员变量。

    03、静态代码块

    静态代码块可以用来初始化静态变量,尽管静态方法也可以在声明的时候直接初始化,但有些时候,我们需要多行代码来完成初始化。

    public class StaticBlockDemo {
        public static List<String> writes = new ArrayList<>();
    
        static {
            writes.add("沉默王二");
            writes.add("沉默王三");
            writes.add("沉默王四");
    
            System.out.println("第一块");
        }
    
        static {
            writes.add("沉默王五");
            writes.add("沉默王六");
    
            System.out.println("第二块");
        }
    }
    

    writes 是一个静态的 ArrayList,所以不太可能在声明的时候完成初始化,因此需要在静态代码块中完成初始化。

    简单小结一下:

    1)一个类可以有多个静态代码块。

    2)静态代码块的解析和执行顺序和它在类中的位置保持一致。为了验证这个结论,可以在 StaticBlockDemo 类中加入空的 main 方法,执行完的结果如下所示:

    第一块
    第二块
    

    04、静态内部类

    Java 允许我们在一个类中声明一个内部类,它提供了一种令人信服的方式,允许我们只在一个地方使用一些变量,使代码更具有条理性和可读性。

    常见的内部类有四种,成员内部类、局部内部类、匿名内部类和静态内部类,限于篇幅原因,前三种不在我们本次文章的讨论范围,以后有机会再细说。

    public class Singleton {
        private Singleton() {}
    
        private static class SingletonHolder {
            public static final Singleton instance = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.instance;
        }
    }
    

    以上这段代码是不是特别熟悉,对,这就是创建单例的一种方式,第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance() 方法时 Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全也能保证 Singleton 类的唯一性。不过,创建单例更优雅的一种方式是使用枚举。

    简单小结一下:

    1)静态内部类不能访问外部类的所有成员变量。

    2)静态内部类可以访问外部类的所有静态变量,包括私有静态变量。

    3)外部类不能声明为 static。

    十六、Java 枚举

    开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum。

    为了证明这一点,我们来新建一个枚举 PlayerType:

    public enum PlayerType {
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }
    

    两个关键字带一个类名,还有大括号,以及三个大写的单词,但没看到继承 Enum 类啊?别着急,心急吃不了热豆腐啊。使用 JAD 查看一下反编译后的字节码,就一清二楚了。

    public final class PlayerType extends Enum
    {
    
        public static PlayerType[] values()
        {
            return (PlayerType[])$VALUES.clone();
        }
    
        public static PlayerType valueOf(String name)
        {
            return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
        }
    
        private PlayerType(String s, int i)
        {
            super(s, i);
        }
    
        public static final PlayerType TENNIS;
        public static final PlayerType FOOTBALL;
        public static final PlayerType BASKETBALL;
        private static final PlayerType $VALUES[];
    
        static 
        {
            TENNIS = new PlayerType("TENNIS", 0);
            FOOTBALL = new PlayerType("FOOTBALL", 1);
            BASKETBALL = new PlayerType("BASKETBALL", 2);
            $VALUES = (new PlayerType[] {
                TENNIS, FOOTBALL, BASKETBALL
            });
        }
    }
    

    看到没?PlayerType 类是 final 的,并且继承自 Enum 类。这些工作我们程序员没做,编译器帮我们悄悄地做了。此外,它还附带几个有用静态方法,比如说 values()valueOf(String name)

    01、内部枚举

    好的,小伙伴们应该已经清楚枚举长什么样子了吧?既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。

    public class Player {
        private PlayerType type;
        public enum PlayerType {
            TENNIS,
            FOOTBALL,
            BASKETBALL
        }
        
        public boolean isBasketballPlayer() {
          return getType() == PlayerType.BASKETBALL;
        }
    
        public PlayerType getType() {
            return type;
        }
    
        public void setType(PlayerType type) {
            this.type = type;
        }
    }
    

    PlayerType 就相当于 Player 的内部类,isBasketballPlayer() 方法用来判断运动员是否是一个篮球运动员。

    由于枚举是 final 的,可以确保在 Java 虚拟机中仅有一个常量对象(可以参照反编译后的静态代码块「static 关键字带大括号的那部分代码」),所以我们可以很安全地使用“==”运算符来比较两个枚举是否相等,参照 isBasketballPlayer() 方法。

    那为什么不使用 equals() 方法判断呢?

    if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
    if(player.getType() == Player.PlayerType.BASKETBALL){};
    

    “==”运算符比较的时候,如果两个对象都为 null,并不会发生 NullPointerException,而 equals() 方法则会。

    另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 equals() 方法则不会。

    02、枚举可用于 switch 语句

    这个我在之前的一篇我去的文章中详细地说明过了,感兴趣的小伙伴可以点击链接跳转过去看一下。

    switch (playerType) {
            case TENNIS:
                return "网球运动员费德勒";
            case FOOTBALL:
                return "足球运动员C罗";
            case BASKETBALL:
                return "篮球运动员詹姆斯";
            case UNKNOWN:
                throw new IllegalArgumentException("未知");
            default:
                throw new IllegalArgumentException(
                        "运动员类型: " + playerType);
    
        }
    

    03、枚举可以有构造方法

    如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。

    public enum PlayerType {
        TENNIS("网球"),
        FOOTBALL("足球"),
        BASKETBALL("篮球");
    
        private String name;
    
        PlayerType(String name) {
            this.name = name;
        }
    }
    

    04、EnumSet

    EnumSet 是一个专门针对枚举类型的 Set 接口的实现类,它是处理枚举类型数据的一把利器,非常高效(内部实现是位向量,我也搞不懂)。

    因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法:

    下面的示例中使用 noneOf() 创建了一个空的 PlayerType 的 EnumSet;使用 allOf() 创建了一个包含所有 PlayerType 的 EnumSet。

    public class EnumSetTest {
        public enum PlayerType {
            TENNIS,
            FOOTBALL,
            BASKETBALL
        }
    
        public static void main(String[] args) {
            EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
            System.out.println(enumSetNone);
    
            EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
            System.out.println(enumSetAll);
        }
    }
    

    程序输出结果如下所示:

    []
    [TENNIS, FOOTBALL, BASKETBALL]
    

    有了 EnumSet 后,就可以使用 Set 的一些方法了:

    05、EnumMap

    EnumMap 是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。

    和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字:

    EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
    

    有了 EnumMap 对象后就可以使用 Map 的一些方法了:

    和 HashMap 的使用方法大致相同,来看下面的例子:

    EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
    enumMap.put(PlayerType.BASKETBALL,"篮球运动员");
    enumMap.put(PlayerType.FOOTBALL,"足球运动员");
    enumMap.put(PlayerType.TENNIS,"网球运动员");
    System.out.println(enumMap);
    
    System.out.println(enumMap.get(PlayerType.BASKETBALL));
    System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
    System.out.println(enumMap.remove(PlayerType.BASKETBALL));
    

    程序输出结果如下所示:

    {TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}
    篮球运动员
    true
    篮球运动员
    

    06、单例

    通常情况下,实现一个单例并非易事,不信,来看下面这段代码

    public class Singleton {  
        private volatile static Singleton singleton; 
        private Singleton (){}  
        public static Singleton getSingleton() {  
        if (singleton == null) {
            synchronized (Singleton.class) { 
            if (singleton == null) {  
                singleton = new Singleton(); 
            }  
            }  
        }  
        return singleton;  
        }  
    }
    

    但枚举的出现,让代码量减少到极致:

    public enum EasySingleton{
        INSTANCE;
    }
    

    完事了,真的超级短,有没有?枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。

    07、枚举可与数据库交互

    我们可以配合 Mybatis 将数据库字段转换为枚举类型。现在假设有一个数据库字段 check_type 的类型如下:

    `check_type` int(1) DEFAULT NULL COMMENT '检查类型(1:未通过、2:通过)',
    

    它对应的枚举类型为 CheckType,代码如下:

    public enum CheckType {
        NO_PASS(0, "未通过"), PASS(1, "通过");
        private int key;
    
        private String text;
    
        private CheckType(int key, String text) {
            this.key = key;
            this.text = text;
        }
    
        public int getKey() {
            return key;
        }
    
        public String getText() {
            return text;
        }
    
        private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
        static {
            for(CheckType d : CheckType.values()){
                map.put(d.key, d);
            }
        }
        
        public static CheckType parse(Integer index) {
            if(map.containsKey(index)){
                return map.get(index);
            }
            return null;
        }
    }
    

    1)CheckType 添加了构造方法,还有两个字段,key 为 int 型,text 为 String 型。

    2)CheckType 中有一个public static CheckType parse(Integer index)方法,可将一个 Integer 通过 key 的匹配转化为枚举类型。

    那么现在,我们可以在 Mybatis 的配置文件中使用 typeHandler 将数据库字段转化为枚举类型。

    <resultMap id="CheckLog" type="com.entity.CheckLog">
      <id property="id" column="id"/>
      <result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
    </resultMap>
    

    其中 checkType 字段对应的类如下:

    public class CheckLog implements Serializable {
    
        private String id;
        private CheckType checkType;
    
        public String getId() {
            return id;
        }
    
        public void setId(String id) {
            this.id = id;
        }
    
        public CheckType getCheckType() {
            return checkType;
        }
    
        public void setCheckType(CheckType checkType) {
            this.checkType = checkType;
        }
    }
    

    CheckTypeHandler 转换器的类源码如下:

    public class CheckTypeHandler extends BaseTypeHandler<CheckType> {
    
        @Override
        public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
            return CheckType.parse(rs.getInt(index));
        }
    
        @Override
        public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
            return CheckType.parse(rs.getInt(index));
        }
    
        @Override
        public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
            return CheckType.parse(cs.getInt(index));
        }
    
        @Override
        public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
            ps.setInt(index, val.getKey());
        }
    }
    

    CheckTypeHandler 的核心功能就是调用 CheckType 枚举类的 parse() 方法对数据库字段进行转换。

    恕我直言,我觉得小伙伴们肯定会用 Java 枚举了,如果还不会,就过来砍我!

    十七、final 关键字

    尽管继承可以让我们重用现有代码,但有时处于某些原因,我们确实需要对可扩展性进行限制,final 关键字可以帮助我们做到这一点。

    01、final 类

    如果一个类使用了 final 关键字修饰,那么它就无法被继承。如果小伙伴们细心观察的话,Java 就有不少 final 类,比如说最常见的 String 类。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence,
                   Constable, ConstantDesc {}
    

    为什么 String 类要设计成 final 的呢?原因大致有以下三个:

    • 为了实现字符串常量池
    • 为了线程安全
    • 为了 HashCode 的不可变性

    更详细的原因,可以查看我之前写的一篇文章

    任何尝试从 final 类继承的行为将会引发编译错误,为了验证这一点,我们来看下面这个例子,Writer 类是 final 的。

    public final class Writer {
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    尝试去继承它,编译器会提示以下错误,Writer 类是 final 的,无法继承。

    不过,类是 final 的,并不意味着该类的对象是不可变的。

    Writer writer = new Writer();
    writer.setName("沉默王二");
    System.out.println(writer.getName()); // 沉默王二
    

    Writer 的 name 字段的默认值是 null,但可以通过 settter 方法将其更改为“沉默王二”。也就是说,如果一个类只是 final 的,那么它并不是不可变的全部条件。

    如果,你想了解不可变类的全部真相,请查看我之前写的文章这次要说不明白immutable类,我就怎么地。突然发现,写系列文章真的妙啊,很多相关性的概念全部涉及到了。我真服了自己了。

    把一个类设计成 final 的,有其安全方面的考虑,但不应该故意为之,因为把一个类定义成 final 的,意味着它没办法继承,假如这个类的一些方法存在一些问题的话,我们就无法通过重写的方式去修复它。

    02、final 方法

    被 final 修饰的方法不能被重写。如果我们在设计一个类的时候,认为某些方法不应该被重写,就应该把它设计成 final 的。

    Thread 类就是一个例子,它本身不是 final 的,这意味着我们可以扩展它,但它的 isAlive() 方法是 final 的:

    public class Thread implements Runnable {
        public final native boolean isAlive();
    }
    

    需要注意的是,该方法是一个本地(native)方法,用于确认线程是否处于活跃状态。而本地方法是由操作系统决定的,因此重写该方法并不容易实现。

    Actor 类有一个 final 方法 show()

    public class Actor {
        public final void show() {
            
        }
    }
    

    当我们想要重写该方法的话,就会出现编译错误:

    如果一个类中的某些方法要被其他方法调用,则应考虑事被调用的方法称为 final 方法,否则,重写该方法会影响到调用方法的使用。

    一个类是 final 的,和一个类不是 final,但它所有的方法都是 final 的,考虑一下,它们之间有什么区别?

    我能想到的一点,就是前者不能被继承,也就是说方法无法被重写;后者呢,可以被继承,然后追加一些非 final 的方法。没毛病吧?看把我聪明的。

    03、final 变量

    被 final 修饰的变量无法重新赋值。换句话说,final 变量一旦初始化,就无法更改。之前被一个小伙伴问过,什么是 effective final,什么是 final,这一点,我在之前的文章也有阐述过,所以这里再贴一下地址:

    http://www.itwanger.com/java/2020/02/14/java-final-effectively.html

    1)final 修饰的基本数据类型

    来声明一个 final 修饰的 int 类型的变量:

    final int age = 18;
    

    尝试将它修改为 30,结果编译器生气了:

    2)final 修饰的引用类型

    现在有一个普通的类 Pig,它有一个字段 name:

    public class Pig {
       private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    在测试类中声明一个 final 修饰的 Pig 对象:

     final Pig pig = new Pig();
    

    如果尝试将 pig 重新赋值的话,编译器同样会生气:

    但我们仍然可以去修改 Pig 的字段值:

    final Pig pig = new Pig();
    pig.setName("特立独行");
    System.out.println(pig.getName()); // 特立独行
    

    3)final 修饰的字段

    final 修饰的字段可以分为两种,一种是 static 的,另外一种是没有 static 的,就像下面这样:

    public class Pig {
       private final int age = 1;
       public static final double PRICE = 36.5;
    }
    

    非 static 的 final 字段必须有一个默认值,否则编译器将会提醒没有初始化:

    static 的 final 字段也叫常量,它的名字应该为大写,可以在声明的时候初始化,也可以通过 static 代码块初始化

    1. final 修饰的参数

    final 关键字还可以修饰参数,它意味着参数在方法体内不能被再修改:

    public class ArgFinalTest {
        public void arg(final int age) {
        }
    
        public void arg1(final String name) {
        }
    }
    

    如果尝试去修改它的话,编译器会提示以下错误:

    。。。。。。

    有些小伙伴可能就忍不住了,这份小白手册有没有 PDF 版可以白嫖啊,那必须得有啊,直接微信搜「沉默王二」回复「小白」就可以了,不要手软,觉得不错的,请多多分享——赠人玫瑰,手有余香哦。

    本文已收录 GitHub,传送门~ ,里面更有大厂面试完整考点,欢迎 Star。

    我是沉默王二,一枚有颜值却靠才华苟且的程序员。关注即可提升学习效率,别忘了三连啊,点赞、收藏、留言,我不挑,嘻嘻

    相关文章

      网友评论

        本文标题:怒肝俩月,新鲜出炉史上最有趣的Java小白手册,第一版(第二部分

        本文链接:https://www.haomeiwen.com/subject/lrqatktx.html