当前位置:网站首页 > 技术博客 > 正文

java抽象类有哪些



“二哥,你这明显加快了更新的频率呀!”三妹对于我最近的肝劲由衷的佩服了起来。

“哈哈,是呀,我要给广大的学弟学妹们一个完整的 Java 学习体系,记住我们的口号,学 Java 就上二哥的 Java 进阶之路。”我对未来充满了信心。

“那就开始吧。”三妹说。

定义抽象类的时候需要用到关键字 ,放在 关键字前,就像下面这样。

关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,这条规约还是值得遵守的,真正做到名如其意。

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

虽然抽象类不能实例化,但可以有子类。子类通过 关键字来继承抽象类。就像下面这样。

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

当我们尝试在一个普通类中定义抽象方法的时候,编译器会有两处错误提示。第一处在类级别上,提示“这个类必须通过 关键字定义”,见下图。

第二处在尝试定义 abstract 的方法上,提示“抽象方法所在的类不是抽象的”,见下图。

抽象类中既可以定义抽象方法,也可以定义普通方法,就像下面这样:

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

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

“二哥,抽象方法我明白了,那什么时候使用抽象方法呢?能给我讲讲它的应用场景吗?”三妹及时的插话道。

“这问题问的恰到好处呀!”我扶了扶眼镜继续说。

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

子类 BasketballPlayer 继承了 AbstractPlayer 类:

也就拥有了 方法。BasketballPlayer 的对象可以直接调用父类的 方法:

子类 FootballPlayer 继承了 AbstractPlayer 类:

也拥有了 方法,FootballPlayer 的对象也可以直接调用父类的 方法:

这样是不是就实现了代码的复用呢?

当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。比如说,AbstractPlayer 抽象类中定义了一个抽象方法 ,表明所有运动员都可以从事某项运动,但需要对应子类去扩展实现,表明篮球运动员打篮球,足球运动员踢足球。

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

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

为了进一步展示抽象类的特性,我们再来看一个具体的示例。

PS:网站评论区说涉及到了文件的读写以及 Java 8 的新特性,不适合新人,如果觉得自己实在是看不懂,跳过,等学了 IO 流再来看也行。如果说是为了复习 Java 基础知识,就不存在这个问题了。

假设现在有一个文件,里面的内容非常简单,只有一个“Hello World”,现在需要有一个读取器将内容从文件中读取出来,最好能按照大写的方式,或者小写的方式来读。

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

  • filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问到。
  • 方法用来读取文件,方法体里面调用了抽象方法 ——需要子类来扩展实现大小写的不同读取方式。

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

小写的方式:

大写的方式:

从文件里面一行一行读取内容的代码被子类复用了。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。

来看一下测试类 FileReaderTest:

在项目的 resource 目录下建一个文本文件,名字叫 helloworld.txt,里面的内容就是“Hello World”。文件的具体位置如下图所示,我用的集成开发环境是 Intellij IDEA。

在 resource 目录下的文件可以通过 的方式获取到 URI 路径,然后就可以取到文本内容了。

输出结果如下所示:

好了,对于抽象类我们简单总结一下:

  • 1、抽象类不能被实例化。
  • 2、抽象类应该至少有一个抽象方法,否则它没有任何意义。
  • 3、抽象类中的抽象方法没有方法体。
  • 4、抽象类的子类必须给出父类中的抽象方法的具体实现,除非该子类也是抽象类。

“完了吗?二哥”三妹似乎还沉浸在聆听教诲的快乐中。

“是滴,这次我们系统化的学习了抽象类,可以说面面俱到了。三妹你可以把代码敲一遍,加强了一些印象,电脑交给你了。”说完,我就跑到阳台去抽烟了。

“呼。。。。。”一个大大的眼圈飘散开来,又是愉快的一天~


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

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

“二哥,开讲之前,先恭喜你呀。我看你朋友圈说《Java进阶之路》开源知识库在 GitHub 上收到了第一笔赞赏呀,虽然只有一块钱,但我也替你感到开心。”三妹的脸上洋溢着自信的微笑,仿佛这钱是打给她的一样。

PS:2021-04-29到2023-02-11期间,《二哥的 Java 进阶之路》收到了 58 笔赞赏,真的非常感谢大家的认可和支持😍,我会继续肝下去的。

“是啊,早上起来的时候看到这条信息,还真的是挺开心的,虽然只有一块钱,但是开源的第一笔,也是我人生当中的第一笔,真的非常感谢这个读者,值得纪念的一天。”我自己也掩饰不住内心的激动。

“有了这份鼓励,我相信你更新下去的动力更足了!”三妹今天说的话真的是特别令人喜欢。

“是呀是呀,让我们开始吧!”

“接口是什么呀?”三妹顺着我的话题及时的插话到。

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

来看一下这段代码反编译后的字节码。

发现没?接口中定义的所有变量或者方法,都会自动添加上 关键字。

接下来,我来一一解释下 Electronic 接口中的核心知识点。

1)接口中定义的变量会在编译的时候自动加上 修饰符(注意看一下反编译后的字节码),也就是说上例中的 LED 变量其实就是一个常量。

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

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

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

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

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

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

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

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

4)接口中允许定义 方法也是从 Java 8 开始的,比如说上例中的 方法,它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。

“为什么要在接口中定义默认方法呢?”三妹好奇地问到。

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

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

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

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

1)接口不允许直接实例化,否则编译器会报错。

需要定义一个类去实现接口,见下例。

然后再实例化。

2)接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,在 包下。

Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。

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

4)接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。

5)接口的变量是隐式 (常量),所以其值无法改变。

“接口可以做什么呢?”三妹见缝插针,问的很及时。

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

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

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

运行后抛出 CloneNotSupportedException:

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

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

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

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

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

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

第三,实现多态

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

多态可以通过继承()的关系实现,也可以通过接口的形式实现。

Shape 接口表示一个形状。

Circle 类实现了 Shape 接口,并重写了 方法。

Square 类也实现了 Shape 接口,并重写了 方法。

然后来看测试类。

这就实现了多态,变量 circleShape、squareShape 的引用类型都是 Shape,但执行 方法的时候,Java 虚拟机知道该去调用 Circle 的 方法还是 Square 的 方法。

说一下多态存在的 3 个前提:

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

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

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

在编程领域,好的设计模式能够让我们的代码事半功倍。在使用接口的时候,经常会用到三种模式,分别是策略模式、适配器模式和工厂模式。

策略模式的思想是,针对一组算法,将每一种算法封装到具有共同接口的实现类中,接口的设计者可以在不影响调用者的情况下对算法做出改变。示例如下:

方法可以接受不同风格的 Coach,并根据所传递的参数对象的不同而产生不同的行为,这被称为“策略模式”。

适配器模式的思想是,针对调用者的需求对原有的接口进行转接。生活当中最常见的适配器就是HDMI(英语:,中文:高清多媒体接口)线,可以同时发送音频和视频信号。适配器模式的示例如下:

Coach 接口中定义了两个方法( 和 ),如果类直接实现该接口的话,就需要对两个方法进行实现。

如果我们只需要对其中一个方法进行实现的话,就可以使用一个抽象类作为中间件,即适配器(AdapterCoach),用这个抽象类实现接口,并对抽象类中的方法置空(方法体只有一对花括号),这时候,新类就可以绕过接口,继承抽象类,我们就可以只对需要的方法进行覆盖,而不是接口中的所有方法。

所谓的工厂模式理解起来也不难,就是什么工厂生产什么,比如说宝马工厂生产宝马,奔驰工厂生产奔驰,A 级学院毕业 A 级教练,C 级学院毕业 C 级教练。示例如下:

有两个接口,一个是 Coach(教练),可以 (指挥球队);另外一个是 CoachFactory(教练学院),能 (教出一名优秀的教练)。然后 ACoach 类实现 Coach 接口,ACoachFactory 类实现 CoachFactory 接口;CCoach 类实现 Coach 接口,CCoachFactory 类实现 CoachFactory 接口。当需要 A 级教练时,就去找 A 级教练学院;当需要 C 级教练时,就去找 C 级教练学院。

依次类推,我们还可以用 BCoach 类实现 Coach 接口,BCoachFactory 类实现 CoachFactory 接口,从而不断地丰富教练的梯队。

“怎么样三妹,一下子接收这么多知识点不容易吧?”

“其实还好啊,二哥你讲的这么细致,我都做好笔记📒了,学习嘛,认真一点,效果就会好很多了。”

三妹这种积极乐观的态度真的让我感觉到“付出就会有收获”,💪🏻。

简单总结一下抽象类和接口的区别。

在 Java 中,通过关键字 定义的类叫做抽象类。Java 是一门面向对象的语言,因此所有的对象都是通过类来描述的;但反过来,并不是所有的类都是用来描述对象的,抽象类就是其中的一种。

以下示例展示了一个简单的抽象类:

我们知道,有抽象方法的类被称为抽象类,也就意味着抽象类中还能有不是抽象方法的方法。这样的类就不能算作纯粹的接口,尽管它也可以提供接口的功能——只能说抽象类是普通类与接口之间的一种中庸之道。

接口(英文:Interface),在 Java 中是一个抽象类型,是抽象方法的集合;接口通过关键字 来定义。接口与抽象类的不同之处在于:

  • 1、抽象类可以有方法体的方法,但接口没有(Java 8 以前)。
  • 2、接口中的成员变量隐式为 ,但抽象类不是的。
  • 3、一个类可以实现多个接口,但只能继承一个抽象类。

以下示例展示了一个简单的接口:

  • 接口是隐式抽象的,所以声明时没有必要使用 关键字;
  • 接口的每个方法都是隐式抽象的,所以同样不需要使用 关键字;
  • 接口中的方法都是隐式 的。

“哦,我理解了哥。那我再问一下,抽象类和接口有什么差别呢?”

“哇,三妹呀,你这个问题恰到好处,问到了点子上。”我不由得为三妹竖起了大拇指。

  • 抽象类可以包含具体方法的实现;而在接口中,方法默认是 public abstract 的,但从 Java 8 开始,接口也可以包含有实现的默认方法和静态方法。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
  • 接口中不能含有静态代码块,而抽象类可以有静态代码块;
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

抽象类是对一种事物的抽象,即对类抽象,继承抽象类的子类和抽象类本身是一种 的关系。而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类 Airplane,将鸟设计为一个类 Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。

此时可以将 飞行 设计为一个接口 Fly,包含方法 fly(),然后 Airplane 和 Bird 分别根据自己的需要实现 Fly 这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承 Airplane 即可,对于鸟也是类似的,不同种类的鸟直接继承 Bird 类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

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

抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过 ppt 里面的模板,如果用模板 A 设计了 ppt B 和 ppt C,ppt B 和 ppt C 公共的部分就是模板 A 了,如果它们的公共部分需要改动,则只需要改动模板 A 就可以了,不需要重新对 ppt B 和 ppt C 进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

“在 Java 中,可以将一个类定义在另外一个类里面或者一个方法里面,这样的类叫做内部类。”我放下手中的枸杞杯,对三妹说,“一般来说,内部类分为成员内部类、局部内部类、匿名内部类和静态内部类。”

成员内部类是最常见的内部类,看下面的代码:

看起来内部类 Wangxiaoer 就好像 Wanger 的一个成员,成员内部类可以无限制访问外部类的所有成员属性。

内部类可以随心所欲地访问外部类的成员,但外部类想要访问内部类的成员,就不那么容易了,必须先创建一个成员内部类的对象,再通过这个对象来访问:

这也就意味着,如果想要在静态方法中访问成员内部类的时候,就必须先得创建一个外部类的对象,因为内部类是依附于外部类的。

这种创建内部类的方式在实际开发中并不常用,因为内部类和外部类紧紧地绑定在一起,使用起来非常不便。

局部内部类是定义在一个方法或者一个作用域里面的类,所以局部内部类的生命周期仅限于作用域内。

局部内部类就好像一个局部变量一样,它是不能被权限修饰符修饰的,比如说 public、protected、private 和 static 等。

匿名内部类是我们平常用得最多的,尤其是启动多线程的时候,会经常用到,并且 IDE 也会帮我们自动生成。

匿名内部类就好像一个方法的参数一样,用完就没了,以至于我们都不需要为它专门写一个构造方法,它的名字也是由系统自动命名的。仔细观察编译后的字节码文件也可以发现,匿名内部类连名字都不配拥有,哈哈,直接借用的外部类,然后 就搞定了。

匿名内部类是唯一一种没有构造方法的类。就上面的写法来说,匿名内部类也不允许我们为其编写构造方法,因为它就像是直接通过 new 关键字创建出来的一个对象。

匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。

静态内部类和成员内部类类似,只是多了一个 static 关键字。

由于 static 关键字的存在,静态内部类是不允许访问外部类中非 static 的变量和方法的,这一点也非常好理解:你一个静态的内部类访问我非静态的成员变量干嘛?

“为什么要使用内部类呢?”三妹问。

三妹这个问题问的非常妙,是时候引经据典了。

在《Think in java》中有这样一句话:

使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

在我们程序设计中有时候会存在一些使用接口很难解决的问题,这个时候我们可以利用内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问题。可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。

使用内部类还能够为我们带来如下特性:

  • 1、内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
  • 2、在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
  • 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
  • 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
  • 5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。

参考链接:https://www.cnblogs.com/dolphin0520/p/3811445.html,作者:Matrix海 子,编辑:沉默王二


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

在谈 Java 面向对象的时候,不得不提到面向对象的三大特征:封装、继承、多态。三大特征紧密联系而又有区别,合理使用继承能大大减少重复代码,提高代码复用性。

“三妹,准备好了没,我们来讲 Java 封装,算是 Java 的三大特征之一,理清楚了,对以后的编程有较大的帮助。”我对三妹说。

“好的,哥,准备好了。”三妹一边听我说,一边迅速地打开了 XMind,看来一边学习一边总结思维导图这个高效的学习方式三妹已经牢记在心了。

封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体

数据被保护在类的内部,尽可能地隐藏内部的实现细节,只保留一些对外接口使之与外部发生联系。

其他对象只能通过已经授权的操作来与这个封装的对象进行交互。也就是说用户是无需知道对象内部的细节(当然也无从知道),但可以通过该对象对外的提供的接口来访问该对象。

使用封装有 4 大好处:

  • 1、良好的封装能够减少耦合。
  • 2、类内部的结构可以自由修改。
  • 3、可以对成员进行更精确的控制。
  • 4、隐藏信息,实现细节。

首先我们先来看两个类。

Husband.java

Wife.java

可以看得出, Husband 类里面的 wife 属性是没有 的,同时 Wife 类的 age 属性也是没有 方法的。至于理由我想三妹你是懂的。

没有哪个女人愿意别人知道她的年龄。

所以封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问。

但是如果一个类没有提供给外界任何可以访问的方法,那么这个类也没有什么意义了。

比如我们将一个房子看做是一个对象,里面有漂亮的装饰,如沙发、电视剧、空调、茶桌等等都是该房子的私有属性,但是如果我们没有那些墙遮挡,是不是别人就会一览无余呢?没有一点儿隐私!

因为存在那个遮挡的墙,我们既能够有自己的隐私而且我们可以随意的更改里面的摆设而不会影响到外面的人。

但是如果没有门窗,一个包裹的严严实实的黑盒子,又有什么存在的意义呢?所以通过门窗别人也能够看到里面的风景。所以说门窗就是房子对象留给外界访问的接口。

通过这个我们还不能真正体会封装的好处。现在我们从程序的角度来分析封装带来的好处。如果我们不使用封装,那么该对象就没有 和 ,那么 Husband 类应该这样写:

我们应该这样来使用它:

但是哪天如果我们需要修改 Husband,例如将 age 修改为 String 类型的呢?你只有一处使用了这个类还好,如果你有几十个甚至上百个这样地方,你是不是要改到崩溃。如果使用了封装,我们完全可以不需要做任何修改,只需要稍微改变下 Husband 类的 方法即可。

其他的地方依然这样引用( )保持不变。

到了这里我们确实可以看出,封装确实可以使我们更容易地修改类的内部实现,而无需修改使用了该类的代码

我们再看这个好处:封装可以对成员变量进行更精确的控制

还是那个 Husband,一般来说我们在引用这个对象的时候是不容易出错的,但是有时你迷糊了,写成了这样:

也许你是因为粗心写成了这样,你发现了还好,如果没有发现那就麻烦大了,谁见过 300 岁的老妖怪啊!

但是使用封装我们就可以避免这个问题,我们对 age 的访问入口做一些控制(setter)如:

上面都是对 setter 方法的控制,其实通过封装我们也能够对对象的出口做出很好的控制。例如性别在数据库中一般都是以 1、0 的方式来存储的,但是在前台我们又不能展示 1、0,这里我们只需要在 方法里面做一些转换即可。

在使用的时候我们只需要使用 sexName 即可实现正确的性别显示。同理也可以用于针对不同的状态做出不同的操作。

“好了,关于封装我们就暂时就聊这么多吧。”我喝了一口普洱茶后,对三妹说。

“好的,哥,我懂了。”

参考链接:https://www.cnblogs.com/chenssy/p/3351835.html,整理:沉默王二

继承(英语:inheritance)是面向对象软件技术中的一个概念。它使得复用以前的代码非常容易。

Java 语言是非常典型的面向对象的语言,在 Java 语言中继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法

我们来举个例子:动物有很多种,是一个比较大的概念。在动物的种类中,我们熟悉的有猫(Cat)、狗(Dog)等动物,它们都有动物的一般特征(比如能够吃东西,能够发出声音),不过又在细节上有区别(不同动物的吃的不同,叫声不一样)。

在 Java 语言中实现 Cat 和 Dog 等类的时候,就需要继承 Animal 这个类。继承之后 Cat、Dog 等具体动物类就是子类,Animal 类就是父类。

三妹,你可能会问为什么需要继承

如果仅仅只有两三个类,每个类的属性和方法很有限的情况下确实没必要实现继承,但事情并非如此,事实上一个系统中往往有很多个类并且有着很多相似之处,比如猫和狗同属动物,或者学生和老师同属人。各个类可能又有很多个相同的属性和方法,这样的话如果每个类都重新写不仅代码显得很乱,代码工作量也很大。

这时继承的优势就出来了:可以直接使用父类的属性和方法,自己也可以有自己新的属性和方法满足拓展,父类的方法如果自己有需求更改也可以重写。这样使用继承不仅大大的减少了代码量,也使得代码结构更加清晰可见

所以这样从代码的层面上来看我们设计这个完整的 Animal 类是这样的:

而 Dog,Cat,Chicken 类可以这样设计:

各自的类继承 Animal 后可以直接使用 Animal 类的属性和方法而不需要重复编写,各个类如果有自己的方法也可很容易地拓展。

继承分为单继承和多继承,Java 语言只支持类的单继承,但可以通过实现接口的方式达到多继承的目的。这个我们之前在讲接口的时候就提到过,这里我们再聊一下。

继承 定义 优缺点 单继承 一个子类只拥有一个父类 优点:在类层次结构上比较清晰
缺点:结构的丰富度有时不能满足使用需求
多继承(Java 不支持,但可以用其它方式满足多继承使用需求) 一个子类拥有多个直接的父类 优点:子类的丰富度很高
缺点:容易造成混乱




单继承,一个子类只有一个父类,如我们上面讲过的 Animal 类和它的子类。单继承在类层次结构上比较清晰,但缺点是结构的丰富度有时不能满足使用需求

多继承,一个子类有多个直接的父类。这样做的好处是子类拥有所有父类的特征,子类的丰富度很高,但是缺点就是容易造成混乱。下图为一个混乱的例子。

Java 虽然不支持多继承,但是 Java 有三种实现多继承效果的方式,分别是内部类、多层继承和实现接口。

内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,可以达到多继承的效果。

多层继承:子类继承父类,父类如果还继承其他的类,那么这就叫多层继承。这样子类就会拥有所有被继承类的属性和方法。

实现接口无疑是满足多继承使用需求的最好方式,一个类可以实现多个接口满足自己在丰富性和复杂环境的使用需求。

类和接口相比,类就是一个实体,有属性和方法,而接口更倾向于一组方法。举个例子,就拿斗罗大陆的唐三来看,他存在的继承关系可能是这样的:

在 Java 中,类的继承是单一继承,也就是说一个子类只能拥有一个父类,所以extends只能继承一个类。其使用语法为:

例如 Dog 类继承 Animal 类,它是这样的:

子类继承父类后,就拥有父类的非私有的属性和方法。如果不明白,请看这个案例,在 IDEA 下创建一个项目,创建一个 test 类做测试,分别创建 Animal 类和 Dog 类,Animal 作为父类写一个 sayHello()方法,Dog 类继承 Animal 类之后就可以调用 sayHello()方法。具体代码为:

点击运行的时候 Dog 子类可以直接使用 Animal 父类的方法。

使用 implements 关键字可以变相使 Java 拥有多继承的特性,使用范围为类实现接口的情况,一个类可以实现多个接口(接口与接口之间用逗号分开)。

我们来看一个案例,创建一个 test2 类做测试,分别创建 doA 接口和 doB 接口,doA 接口声明 sayHello()方法,doB 接口声明 eat()方法,创建 Cat2 类实现 doA 和 doB 接口,并且在类中需要重写 sayHello()方法和 eat()方法。具体代码为:

Cat 类实现 doA 和 doB 接口的时候,需要实现其声明的方法,点击运行结果如下,这就是一个类实现接口的简单案例:

继承的主要内容就是子类继承父类,并重写父类的方法。使用子类的属性或方法时候,首先要创建一个对象,而对象通过构造方法去创建,在构造方法中我们可能会调用子父类的一些属性和方法,所以就需要提前掌握 this 和 super 关键字。

创建完这个对象之后,再调用重写父类后的方法,注意重写和重载的区别。

后面会详细讲,这里先来简单了解一下。

this 和 super 关键字是继承中非常重要的知识点,分别表示当前对象的引用和父类对象的引用,两者有很大相似又有一些区别。

this 表示当前对象,是指向自己的引用。

super 表示父类对象,是指向父类的引用。

构造方法是一种特殊的方法,它是一个与类同名的方法。在继承中构造方法是一种比较特殊的方法(比如不能继承),所以要了解和学习在继承中构造方法的规则和要求。

继承中的构造方法有以下几点需要注意:

父类的构造方法不能被继承:

因为构造方法语法是与类同名,而继承则不更改方法名,如果子类继承父类的构造方法,那明显与构造方法的语法冲突了。比如 Father 类的构造方法名为 Father(),Son 类如果继承 Father 类的构造方法 Father(),那就和构造方法定义:构造方法与类同名冲突了,所以在子类中不能继承父类的构造方法,但子类会调用父类的构造方法。

子类的构造过程必须调用其父类的构造方法:

Java 虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加。而 Java 虚拟机构造父类对象会执行父类的构造方法,所以子类构造方法必须调用 super()即父类的构造方法。就比如一个简单的继承案例应该这么写:

如果子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。

你可能有时候在写继承的时候子类并没有使用 super()调用,程序依然没问题,其实这样是为了节省代码,系统执行时会自动添加父类的无参构造方式,如果不信的话我们对上面的类稍作修改执行:

方法重写也就是子类中出现和父类中一模一样的方法(包括返回值类型,方法名,参数列表),它建立在继承的基础上。你可以理解为方法的外壳不变,但是核心内容重写

在这里提供一个简单易懂的方法重写案例:

其中 注解显示声明该方法为注解方法,可以帮你检查重写方法的语法正确性,当然如果不加也是可以的,但建议加上。

如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。

重载可以通常理解为完成同一个事情的方法名相同,但是参数列表不同其他条件也可能不同。一个简单的方法重载的例子,类 E3 中的 add()方法就是一个重载方法。

Java 修饰符的作用就是对类或类成员进行修饰或限制,每个修饰符都有自己的作用,而在继承中可能有些特殊修饰符使得被修饰的属性或方法不能被继承,或者继承需要一些其他的条件。

Java 语言提供了很多修饰符,修饰符用来定义类、方法或者变量,通常放在语句的最前端。主要分为以下两类:

  • 访问权限修饰符,也就是 public、private、protected 等
  • 非访问修饰符,也就是 static、final、abstract 等

Java 子类重写继承的方法时,不可以降低方法的访问权限子类继承父类的访问修饰符作用域不能比父类小,也就是更加开放,假如父类是 protected 修饰的,其子类只能是 protected 或者 public,绝对不能是 default(默认的访问范围)或者 private。所以在继承中需要重写的方法不能使用 private 修饰词修饰。

如果还是不太清楚可以看几个小案例就很容易搞懂,写一个 A1 类中用四种修饰词实现四个方法,用子类 A2 继承 A1,重写 A1 方法时候你就会发现父类私有方法不能重写,非私有方法重写使用的修饰符作用域不能变小(大于等于)。

正确的案例应该为:

还要注意的是,继承当中子类抛出的异常必须是父类抛出的异常或父类抛出异常的子异常。下面的一个案例四种方法测试可以发现子类方法的异常不可大于父类对应方法抛出异常的范围。

正确的案例应该为:

访问修饰符用来控制访问权限,而非访问修饰符每个都有各自的作用,下面针对 static、final、abstract 修饰符进行介绍。

static 修饰符

static 翻译为“静态的”,能够与变量,方法和类一起使用,称为静态变量,静态方法(也称为类变量、类方法)。如果在一个类中使用 static 修饰变量或者方法的话,它们可以直接通过类访问,不需要创建一个类的对象来访问成员。

我们在设计类的时候可能会使用静态方法,有很多工具类比如,等类里面就写了很多静态方法。

可以看以下的案例证明上述规则:

源代码为:

final 修饰符

final 变量:

  • final 表示"最后的、最终的"含义,变量一旦赋值后,不能被重新赋值。被 final 修饰的实例变量必须显式指定初始值(即不能只声明)。final 修饰符通常和 static 修饰符一起使用来创建类常量。

final 方法:

  • 父类中的 final 方法可以被子类继承,但是不能被子类重写。声明 final 方法的主要目的是防止该方法的内容被修改。

final 类:

  • final 类不能被继承,没有类能够继承 final 类的任何特性。

所以无论是变量、方法还是类被 final 修饰之后,都有代表最终、最后的意思。内容无法被修改。

abstract 修饰符

abstract 英文名为“抽象的”,主要用来修饰类和方法,称为抽象类和抽象方法。

抽象方法:有很多不同类的方法是相似的,但是具体内容又不太一样,所以我们只能抽取他的声明,没有具体的方法体,即抽象方法可以表达概念但无法具体实现。

抽象类有抽象方法的类必须是抽象类,抽象类可以表达概念但是无法构造实体的类。

比如我们可以这样设计一个 People 抽象类以及一个抽象方法,在子类中具体完成:

提到 Java 继承,不得不提及所有类的根类:Object(java.lang.Object)类,如果一个类没有显式声明它的父类(即没有写 extends xx),那么默认这个类的父类就是 Object 类,任何类都可以使用 Object 类的方法,创建的类也可和 Object 进行向上、向下转型,所以 Object 类是掌握和理解继承所必须的知识点。

Java 向上和向下转型在 Java 中运用很多,也是建立在继承的基础上,所以 Java 转型也是掌握和理解继承所必须的知识点。

  1. Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类。
  2. Java 中,所有的对象都拥有 Object 的默认方法。
  3. Object 类有一个构造方法,并且是无参构造方法

Object 是 Java 所有类的父类,是整个类继承结构的顶端,也是最抽象的一个类。

像 toString()、equals()、hashCode()、wait()、notify()、getClass()等都是 Object 的方法。你以后可能会经常碰到,但其中遇到更多的就是 toString()方法和 equals()方法,我们经常需要重写这两种方法满足我们的使用需求。

toString()方法表示返回该对象的字符串,由于各个对象构造不同所以需要重写,如果不重写的话默认返回格式。

如果重写 toString()方法后直接调用 toString()方法就可以返回我们自定义的该类转成字符串类型的内容输出,而不需要每次都手动的拼凑成字符串内容输出,大大简化输出操作。

equals()方法主要比较两个对象是否相等,因为对象的相等不一定非要严格要求两个对象地址上的相同,有时内容上的相同我们就会认为它相等,比如 String 类就重写了 euqals()方法,通过字符串的内容比较是否相等。

向上转型 : 通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换。用一张图就能很好地表示向上转型的逻辑:

父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。

向下转型 : 通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型,这里也用一张图就能很好表示向下转型的逻辑:

子类引用变量指向父类引用变量指向的对象后(一个 Son()对象),就完成向下转型,就可以调用一些子类特有而父类没有的方法 。

在这里写一个向上转型和向下转型的案例:

在 Java 继承中,父子类初始化先后顺序为:

  1. 父类中静态成员变量和静态代码块
  2. 子类中静态成员变量和静态代码块
  3. 父类中普通成员变量和代码块,父类的构造方法
  4. 子类中普通成员变量和代码块,子类的构造方法

总的来说,就是静态>非静态,父类>子类,非构造方法>构造方法。同一类别(例如普通变量和普通代码块)成员变量和代码块执行从前到后,需要注意逻辑。

这个也不难理解,静态变量也称类变量,可以看成一个全局变量,静态成员变量和静态代码块在类加载的时候就初始化,而非静态变量和代码块在对象创建的时候初始化。所以静态快于非静态初始化。

而在创建子类对象的时候需要先创建父类对象,所以父类优先于子类。

而在调用构造方法的时候,是对成员变量进行一些初始化操作,所以普通成员变量和代码块优于构造方法执行。

至于更深层次为什么这个顺序,就要更深入了解 JVM 执行流程啦。下面一个测试代码为:

执行结果:

Java 的多态是指在面向对象编程中,同一个类的对象在不同情况下表现出来的不同行为和状态。

  • 子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
  • 子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
  • 可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。

多态的目的是为了提高代码的灵活性和可扩展性,使得代码更容易维护和扩展。

比如说,通过允许子类继承父类的方法并重写,增强了代码的复用性。

再比如说多态可以实现动态绑定,这意味着程序在运行时再确定对象的方法调用也不迟。

“光说理论很枯燥,我们再通过代码来具体地分析一下。”

在我的印象里,西游记里的那段孙悟空和二郎神的精彩对战就能很好的解释“多态”这个词:一个孙悟空,能七十二变;一个二郎神,也能七十二变;他们都可以变成不同的形态,只需要悄悄地喊一声“变”。

Java 的多态是什么?其实就是一种能力——同一个行为具有不同的表现形式;换句话说就是,执行一段代码,Java 在运行时能根据对象类型的不同产生不同的结果。和孙悟空和二郎神都只需要喊一声“变”,然后就变了,并且每次变得还不一样;一个道理。

多态的前提条件有三个:

  • 子类继承父类
  • 子类重写父类的方法
  • 父类引用指向子类的对象

多态的一个简单应用,来看程序清单 1-1:

现在,我们来思考一个问题:程序清单 1-1 在执行 时,由于编译器只有一个 Wanger 引用,它怎么知道究竟该调用父类 Wanger 的 方法,还是子类 Wangxiaoer 的 方法呢?

答案是在运行时根据对象的类型进行后期绑定,编译器在编译阶段并不知道对象的类型,但是 Java 的方法调用机制能找到正确的方法体,然后执行,得到正确的结果。

多态机制提供的一个重要的好处就是程序具有良好的扩展性。来看程序清单 2-1:

在程序清单 2-1 中,我们在 Wanger 类中增加了 方法,在 Wangxiaoer 类中增加了 方法,但这丝毫不会影响到 方法的调用。

方法忽略了周围代码发生的变化,依然正常运行。这让我想起了金庸《倚天屠龙记》里九阳真经的口诀:“他强由他强,清风拂山岗;他横由他横,明月照大江。”

多态的这个优秀的特性,让我们在修改代码的时候不必过于紧张,因为多态是一项让程序员“将改变的与未改变的分离开来”的重要特性。

在构造方法中调用多态方法,会产生一个奇妙的结果,我们来看程序清单 3-1:

从输出结果上看,是不是有点诧异?明明在创建 Wangxiaosan 对象的时候,年龄传递的是 4,但输出结果既不是“老子上幼儿园的年龄是 3 岁半”,也不是“我小三上幼儿园的年龄是:4”。

为什么?

因为在创建子类对象时,会先去调用父类的构造方法,而父类构造方法中又调用了被子类覆盖的多态方法,由于父类并不清楚子类对象中的字段值是什么,于是把 int 类型的属性暂时初始化为 0,然后再调用子类的构造方法(子类构造方法知道王小二的年龄是 4)。

向下转型是指将父类引用强转为子类类型;这是不安全的,因为有的时候,父类引用指向的是父类对象,向下转型就会抛出 ClassCastException,表示类型转换失败;但如果父类引用指向的是子类对象,那么向下转型就是成功的。

来看程序清单 4-1:

好啦,三妹,本次继承就介绍到这里啦,Java 面向对象三大特征封装继承多态——优秀的你已经掌握。

  • 封装:是对类的封装,封装是对类的属性和方法进行封装,只对外暴露方法而不暴露具体使用细节,所以我们一般设计类成员变量时候大多设为私有而通过一些 get、set 方法去读写。
  • 继承:子类继承父类,即“子承父业”,子类拥有父类除私有的所有属性和方法,自己还能在此基础上拓展自己新的属性和方法。主要目的是复用代码
  • 多态:多态是同一个行为具有多个不同表现形式或形态的能力。即一个父类可能有若干子类,各子类实现父类方法有多种多样,调用父类方法时,父类引用变量指向不同子类实例而执行不同方法,这就是所谓父类方法是多态的。

最后送你一张图捋一捋其中的关系吧。

bigsai:封装继承多态
bigsai:封装继承多态

“好的,二哥,我来消化一下,今天内容真不少。你先去休息一下。”三妹回应到。

参考链接:https://bbs.huaweicloud.com/blogs/,作者:bigsai,整理:沉默王二


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

“哥,被喊大舅子的感觉怎么样啊?”三妹不怀好意地对我说,她眼睛里充满着不屑。

“说实话,这种感觉还不错。”我有点难为情的回答她,“不过,有一点令我感到些许失落。大家的焦点似乎都是你的颜值,完全忽略了我的盛世美颜啊!”

“哥,你想啥呢,那是因为你文章写得好,不然谁认识我是谁啊!有你这样的哥哥,我还是挺自豪的。”三妹郑重其事地说,“话说今天咱学啥呢?”

“三妹啊,你这句话说得我喜欢。今天来学习一下 Java 中的 this 关键字吧。”喝了一口农夫山泉后,我对三妹说。

“this 关键字有很多种用法,其中最常用的一个是,它可以作为引用变量,指向当前对象。”我面带着朴实无华的微笑继续说,“除此之外, this 关键字还可以完成以下工作。”

  • 调用当前类的方法;
  • 可以调用当前类的构造方法;
  • this 可以作为参数在方法中传递;
  • this 可以作为参数在构造方法中传递;
  • this 可以作为方法的返回值,返回当前类的对象。

“三妹,来看下面这段代码。”话音刚落,我就在键盘上噼里啪啦一阵敲。

“在上面的例子中,构造方法的参数名和实例变量名相同,由于没有使用 this 关键字,所以无法为实例变量赋值。”我抬起右手的食指,指着屏幕上的 name 和 age 对着三妹说。

“来看一下程序的输出结果。”

“从结果中可以看得出来,尽管创建对象的时候传递了参数,但实例变量并没有赋值。这是因为如果构造方法中没有使用 this 关键字的话,name 和 age 指向的并不是实例变量而是参数本身。”我把脖子扭向右侧,看着三妹说。

“那怎么解决这个问题呢?哥。”三妹着急地问。

“如果参数名和实例变量名产生了冲突.....”我正准备给出答案,三妹打断了我。

“难道用 this 吗?”三妹脱口而出。

“哇,越来越棒了呀,你。”我感觉三妹在学习 Java 这条道路上逐渐有了自己主动思考的意愿。

“是的,来看加上 this 关键字后的代码。”

安静的屋子里又响起了一阵噼里啪啦的键盘声。

“再来看一下程序的输出结果。”

“这次,实例变量有值了,在构造方法中, 指向的就是实例变量,而不再是参数本身了。”我慢吞吞地说着,“当然了,如果参数名和实例变量名不同的话,就不必使用 this 关键字,但我建议使用 this 关键字,这样的代码更有意义。”

“仔细听,三妹,看我敲键盘的速度是不是够快。”

“仔细瞧,三妹,上面这段代码中没有见到 this 关键字吧?”我面带着神秘的微笑,准备给三妹变个魔术。

“确实没有,哥,我确认过了。”

“那接下来,神奇的事情就要发生了。”我突然感觉刘谦附身了。

我快速的在 classes 目录下找到 InvokeCurrentClassMethod.class 文件,然后双击打开(IDEA 默认会使用 FernFlower 打开字节码文件)。

“瞪大眼睛仔细瞧,三妹, 关键字是不是出现了?”

“哇,真的呢,好神奇啊!”三妹为了配合我的演出,也是十二分的卖力。

“我们可以在一个类中使用 this 关键字来调用另外一个方法,如果没有使用的话,编译器会自动帮我们加上。”我对自己深厚的编程功底充满自信,“在源代码中, 在调用 的时候并没有使用 this 关键字,但通过反编译后的字节码可以看得到。”

“再来看下面这段代码。”

“在有参构造方法 中,使用了 来调用无参构造方法 。”这次,我换成了左手的食指,指着屏幕对三妹说,“ 可用于调用当前类的构造方法——构造方法可以重用了。”

“来看一下输出结果。”

“真的啊,无参构造方法也被调用了,所以程序输出了 hello。”三妹看到输出结果后不假思索地说。

“也可以在无参构造方法中使用 并传递参数来调用有参构造方法。”话音没落,我就在键盘上敲了起来,“来看下面这段代码。”

“再来看一下程序的输出结果。”

“不过,需要注意的是, 必须放在构造方法的第一行,否则就报错了。”

“来看下面这段代码。”

“ 关键字可以作为参数在方法中传递,此时,它指向的是当前类的对象。”一不小心,半个小时过去了,我感到嗓子冒烟,于是赶紧又喝了一口水,润润嗓子后继续说道。

“来看一下输出结果,你就明白了,三妹。”

“ 调用了 ,并传递了参数 this, 中打印了当前对象的字符串。 方法中打印了 thisAsParam 对象的字符串。从输出结果中可以看得出来,两者是同一个对象。”

“继续来看代码。”

“在构造方法 中,我们使用 this 关键字作为参数传递给了 Data 对象,它其实指向的就是 这个对象。”

“ 关键字也可以作为参数在构造方法中传递,它指向的是当前类的对象。当我们需要在多个类中使用一个对象的时候,这非常有用。”

“来看一下输出结果。”

“需要休息会吗?三妹”

“没事的,哥,我的注意力还是很集中的,你继续讲吧。”

“好的,那来继续看代码。”

“ 方法返回了 this 关键字,指向的就是 这个对象,所以可以紧接着调用 方法——达到了链式调用的目的,这也是 this 关键字非常经典的一种用法。”

“链式调用的形式在 JavaScript 代码更加常见。”为了向三妹证实这一点,我打开了 jQuery 的源码。

“原来这么多链式调用啊!”三妹感叹到。

“是的。”我点点头,然后指着 方法的返回值对三妹说,“需要注意的是, 关键字作为方法的返回值的时候,方法的返回类型为类的类型。”

“来看一下输出结果。”

“那么,关于 this 关键字的介绍,就到此为止了。”我活动了一下僵硬的脖子后,对三妹说,“如果你学习劲头还可以的话,我们顺带把 super 关键字捎带着过一下,怎么样?”

“不用了吧,听说 super 关键字更简单,我自己看看就行了,不用你讲了!”

“不不不,三妹啊,你得假装听一下,不然我怎么向读者们交差。”

“噢噢噢噢。”三妹意味深长地笑了。

“super 关键字的用法主要有三种。”

  • 指向父类对象;
  • 调用父类的方法;
  • 可以调用父类的构造方法。

“其实和 this 有些相似,只不过用意不大相同。”我端起水瓶,咕咚咕咚又喝了几大口,好渴。“每当创建一个子类对象的时候,也会隐式的创建父类对象,由 super 关键字引用。”

“如果父类和子类拥有同样名称的字段,super 关键字可以用来访问父类的同名字段。”

“来看下面这段代码。”

“父类 Animal 中有一个名为 color 的字段,子类 Dog 中也有一个名为 color 的字段,子类的 方法中,通过 super 关键字可以访问父类的 color。”

“来看一下输出结果。”

“当子类和父类的方法名相同时,可以使用 super 关键字来调用父类的方法。换句话说,super 关键字可以用于方法重写时访问到父类的方法。”

“瞧,三妹。父类 Animal 和子类 Dog 中都有一个名为 的方法,通过 可以访问到父类的 方法。”

等三妹在自我消化的时候,我在键盘上又敲完了一串代码。

“子类 Dog 的构造方法中,第一行代码为 ,它就是用来调用父类的构造方法的。”

“来看一下输出结果。”

“当然了,在默认情况下, 是可以省略的,编译器会主动去调用父类的构造方法。也就是说,子类即使不使用 主动调用父类的构造方法,父类的构造方法仍然会先执行。”

“输出结果和之前一样。”

“ 也可以用来调用父类的有参构造方法,这样可以提高代码的可重用性。”

“Emp 类继承了 Person 类,也就继承了 id 和 name 字段,当在 Emp 中新增了 salary 字段后,构造方法中就可以使用 来调用父类的有参构造方法。”

“来看一下输出结果。”

三妹点了点头,所有所思。


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

“哥,你牙龈肿痛轻点没?周一的《教妹学 Java》(二哥的Java进阶之路前身)你都没有更新,偷懒了呀!”三妹关心地问我。

“今天周四了,吃了三天的药,疼痛已经减轻不少,咱妈还给我打了电话,让我买点牛黄解毒片下下火。”我面带着微笑对三妹说,“学习可不能落下,今天我们来学 Java 中 关键字吧。”

“static 是 Java 中比较难以理解的一个关键字,也是各大公司的面试官最喜欢问到的一个知识点之一。”我喝了一口咖啡继续说道。

“既然是面试重点,那我可得好好学习下。”三妹连忙说。

“static 关键字的作用可以用一句话来描述:‘方便在没有创建对象的情况下进行调用,包括变量和方法’。也就是说,只要类被加载了,就可以通过类名进行访问。”我扶了扶沉重眼镜,继续说到,“static 可以用来修饰类的成员变量,以及成员方法。我们一个个来看。”

“如果在声明变量的时候使用了 static 关键字,那么这个变量就被称为静态变量。静态变量只在类加载的时候获取一次内存空间,这使得静态变量很节省内存空间。”家里的暖气有点足,我跑去开了一点窗户后继续说道。

“来考虑这样一个 Student 类。”话音刚落,我就在键盘上噼里啪啦一阵敲。

这段代码敲完后,我对三妹说:“假设郑州大学录取了一万名新生,那么在创建一万个 Student 对象的时候,所有的字段(name、age 和 school)都会获取到一块内存。学生的姓名和年纪不尽相同,但都属于郑州大学,如果每创建一个对象,school 这个字段都要占用一块内存的话,就很浪费,对吧?三妹。”

“因此,最好将 school 这个字段设置为 static,这样就只会占用一块内存,而不是一万块。”

安静的房子里又响起了一阵噼里啪啦的键盘声。

“瞧,三妹。s1 和 s2 这两个引用变量存放在栈区(stack),沉默王二+18 这个对象和沉默王三+16 这个对象存放在堆区(heap),school 这个静态变量存放在静态区。”

“等等,哥,栈、堆、静态区?”三妹的脸上塞满了疑惑。

“哦哦,别担心,三妹,画幅图你就全明白了。”说完我就打开 draw.io 这个网址,认真地画起了图。

“现在,是不是一下子就明白了?”看着这幅漂亮的手绘图,我心里有点小开心。

“哇,哥,惊艳了呀!”三妹也不忘拍马屁,给我了一个大大的赞。

“好了,三妹,我们来看下面这串代码。”

“我们创建一个成员变量 count,并且在构造函数中让它自增。因为成员变量会在创建对象的时候获取内存,因此每一个对象都会有一个 count 的副本, count 的值并不会随着对象的增多而递增。”

我在侃侃而谈,而三妹似乎有些不太明白。

“没关系,三妹,你先盲猜一下,这段代码输出的结果是什么?”

“按照你的逻辑,应该输出三个 1?是这样吗?”三妹眨眨眼,有点不太自信地回答。

“哎呀,不错哟。”

我在 IDEA 中点了一下运行按钮,程序跑了起来。

“每创建一个 Counter 对象,count 的值就从 0 自增到 1。三妹,想一下,如果 count 是静态的呢?”

“我不知道啊。”

“嗯,来看下面这段代码。”

“来看一下输出结果。”

“简单解释一下哈,由于静态变量只会获取一次内存空间,所以任何对象对它的修改都会得到保留,所以每创建一个对象,count 的值就会加 1,所以最终的结果是 3,明白了吧?三妹。这就是静态变量和成员变量之间的差别。”

“另外,需要注意的是,由于静态变量属于一个类,所以不要通过对象引用来访问,而应该直接通过类名来访问,否则编译器会发出警告。”

“说完静态变量,我们来说静态方法。”说完,我准备点一支华子来抽,三妹阻止了我,她指一指烟盒上的「吸烟有害身体健康」,我笑了。

“好吧。”我只好喝了一口咖啡继续说,“如果方法上加了 static 关键字,那么它就是一个静态方法。”

“静态方法有以下这些特征。”

  • 静态方法属于这个类而不是这个类的对象;
  • 调用静态方法的时候不需要创建这个类的对象;
  • 静态方法可以访问静态变量。

“来,继续上代码”

“仔细听,三妹。 方法就是一个静态方法,所以它可以直接访问静态变量 school,把它的值更改为河南大学;并且,可以通过类名直接调用 方法,就像 这样。”

“来看一下程序的输出结果吧。”

“需要注意的是,静态方法不能访问非静态变量和调用非静态方法。你看,三妹,我稍微改动一下代码,编译器就会报错。”

“先是在静态方法中访问非静态变量,编译器不允许。”

“然后在静态方法中访问非静态方法,编译器同样不允许。”

“关于静态方法的使用,这下清楚了吧,三妹?”

看着三妹点点头,我欣慰地笑了。

“哥,我想到了一个问题,为什么 main 方法是静态的啊?”没想到,三妹串联知识点的功力还是不错的。

“如果 main 方法不是静态的,就意味着 Java 虚拟机在执行的时候需要先创建一个对象才能调用 main 方法,而 main 方法作为程序的入口,创建一个额外的对象显得非常多余。”我不假思索的回答令三妹感到非常的钦佩。

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

“三妹,站起来活动一下,我的脖子都有点僵硬了。”

我们一起走到窗户边,映入眼帘的是从天而降的雪花。三妹和我都高兴坏了,迫不及待地打开窗口,伸出手去触摸雪花的温度,那种稍纵即逝的冰凉,真的舒服极了。

“北国风光,千里冰封,万里雪飘。望长城内外,惟余莽莽;大河上下,顿失滔滔。山舞银蛇,原驰蜡象,欲与天公试比高。须晴日,看红装素裹,分外妖娆。。。。。。”三妹竟然情不自禁地朗诵起了《沁园春·雪》。

确实令人欣喜,这是 2020 年洛阳的第一场雪,的确令人感到开心。

片刻之后。

“除了静态变量和静态方法,static 关键字还有一个重要的作用。”我心情愉悦地对三妹说,“用一个 static 关键字,外加一个大括号括起来的代码被称为静态代码块。”

“就像下面这串代码。”

“静态代码块通常用来初始化一些静态变量,它会优先于 方法执行。”

“来看一下程序的输出结果吧。”

“二哥,既然静态代码块先于 方法执行,那没有 方法的 Java 类能执行成功吗?”三妹的脑回路越来越令我敬佩了。

“Java 1.6 是可以的,但 Java 7 开始就无法执行了。”我胸有成竹地回答到。

“在命令行中执行 的时候,会抛出 NoClassDefFoundError 的错误。”

“三妹,来看下面这个例子。”

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

“静态代码块在初始集合的时候,真的非常有用。在实际的项目开发中,通常使用静态代码块来加载配置文件到内存当中。”

“三妹啊,除了以上只写,static 还有一个不太常用的功能——静态内部类。”

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

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

“来看下面这个例子。”三妹有点走神,我敲了敲她的脑袋后继续说。

“三妹,打起精神,马上就结束了。”

“哦哦,这段代码看起来很别致啊,哥。”

“是的,三妹,这段代码在以后创建单例的时候还会见到。”

“第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 方法时 Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。不过,创建单例更优雅的一种方式是使用枚举,以后再讲给你听。”

“需要注意的是。第一,静态内部类不能访问外部类的所有成员变量;第二,静态内部类可以访问外部类的所有静态变量,包括私有静态变量。第三,外部类不能声明为 static。”

“三妹,你看,在 Singleton 类上加 static 后,编译器就提示错误了。”

三妹点了点头,所有所思。


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

“哥,今天学什么呢?”

“今天学一个重要的关键字——final。 ”我面带着朴实无华的微笑回答着她,“对了,三妹,你打算考研吗?”

“还没想过,我今年才大一呢,到时候再说吧,你决定。”

“好吧。”我摊摊手,表示很无辜,真的是所有的决定都交给我这个哥哥了,如果决定错了,锅得背上。

“好了,我们先来看 final 修饰的变量吧!”

“被 final 修饰的变量无法重新赋值。换句话说,final 变量一旦初始化,就无法更改。”

“来看这行代码。”

“当尝试将 age 的值修改为 30 的时候,编译器就生气了。”

“再来看这段代码。”

“这是一个很普通的 Java 类,它有一个字段 name。”

“然后,我们创建一个测试类,并声明一个 final 修饰的 Pig 对象。”

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

“但我们仍然可以去修改 pig 对象的 name。”

“另外,final 修饰的成员变量必须有一个默认值,否则编译器将会提醒没有初始化。”

“final 和 static 一起修饰的成员变量叫做常量,常量名必须全部大写。”

“有时候,我们还会用 final 关键字来修饰参数,它意味着参数在方法体内不能被再修改。”

“来看下面这段代码。”

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

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

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

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

“来看这段代码。”

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

“三妹,来问你一个问题吧。”正想趁三妹回答问题的时候喝口水。

“你说吧,哥。”

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

“我能想到的一点,就是前者不能被继承,也就是说方法无法被重写;后者呢,可以被继承,然后追加一些非 final 的方法。”还没等我把水咽下去,三妹就回答好了,着实惊呆了我。

“嗯嗯嗯,没毛病没毛病,进步很大啊!”

“那必须啊,谁叫我是你妹呢。”

“如果一个类使用了 final 关键字修饰,那么它就无法被继承.....”

“等等,哥,我知道,String 类就是一个 final 类。”还没等我说完,三妹就抢着说到。

“说得没毛病。”

“那三妹你知道为什么 String 类要设计成 final 吗?”

“这个还真不知道。”三妹的表情透露出这种无奈。

“原因大致有 3 个。”

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

“想了解更详细的原因,可以一会看看我之前写的这篇文章。”

为什么 Java 字符串是不可变的?

“任何尝试从 final 类继承的行为将会引发编译错误。来看这段代码。”

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

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

“来看这段代码。”

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

“关于不可变类,我们留到后面来细讲。”

不可变类

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

“三妹,final 关键字我们就学到这里吧,你一会再学习一下 Java 字符串为什么是不可变的和不可变类。”我揉一揉犯困的双眼,疲惫地给三妹说,“学完这两个知识点,你会对 final 的认知更清晰一些。”

“好的,二哥,我这就去学习去。你去休息会。”

我起身站到阳台上,看着窗外的车水马龙,不一会儿,就发起来呆。

“好想去再看一场周杰伦的演唱会,不知道 2021 有没有这个机会。”

我心里这样想着,天渐渐地暗了下来。


GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

  • 上一篇: 什么是协程
  • 下一篇: 内存tras怎么算
  • 版权声明


    相关文章:

  • 什么是协程2025-07-05 20:30:04
  • esxi9.0发布时间2025-07-05 20:30:04
  • python assert函数用法2025-07-05 20:30:04
  • json里注释2025-07-05 20:30:04
  • 图像滤波算法2025-07-05 20:30:04
  • 内存tras怎么算2025-07-05 20:30:04
  • 游戏编程简单2025-07-05 20:30:04
  • b*树索引2025-07-05 20:30:04
  • usb连接网线转换器驱动下载2025-07-05 20:30:04
  • aop切面类2025-07-05 20:30:04