“在 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 转型也是掌握和理解继承所必须的知识点。
- Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类。
- Java 中,所有的对象都拥有 Object 的默认方法。
- Object 类有一个构造方法,并且是无参构造方法。
Object 是 Java 所有类的父类,是整个类继承结构的顶端,也是最抽象的一个类。
像 toString()、equals()、hashCode()、wait()、notify()、getClass()等都是 Object 的方法。你以后可能会经常碰到,但其中遇到更多的就是 toString()方法和 equals()方法,我们经常需要重写这两种方法满足我们的使用需求。
toString()方法表示返回该对象的字符串,由于各个对象构造不同所以需要重写,如果不重写的话默认返回格式。
如果重写 toString()方法后直接调用 toString()方法就可以返回我们自定义的该类转成字符串类型的内容输出,而不需要每次都手动的拼凑成字符串内容输出,大大简化输出操作。
equals()方法主要比较两个对象是否相等,因为对象的相等不一定非要严格要求两个对象地址上的相同,有时内容上的相同我们就会认为它相等,比如 String 类就重写了 euqals()方法,通过字符串的内容比较是否相等。
向上转型 : 通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换。用一张图就能很好地表示向上转型的逻辑:
父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。
向下转型 : 通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型,这里也用一张图就能很好表示向下转型的逻辑:
子类引用变量指向父类引用变量指向的对象后(一个 Son()对象),就完成向下转型,就可以调用一些子类特有而父类没有的方法 。
在这里写一个向上转型和向下转型的案例:
在 Java 继承中,父子类初始化先后顺序为:
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造方法
- 子类中普通成员变量和代码块,子类的构造方法
总的来说,就是静态>非静态,父类>子类,非构造方法>构造方法。同一类别(例如普通变量和普通代码块)成员变量和代码块执行从前到后,需要注意逻辑。
这个也不难理解,静态变量也称类变量,可以看成一个全局变量,静态成员变量和静态代码块在类加载的时候就初始化,而非静态变量和代码块在对象创建的时候初始化。所以静态快于非静态初始化。
而在创建子类对象的时候需要先创建父类对象,所以父类优先于子类。
而在调用构造方法的时候,是对成员变量进行一些初始化操作,所以普通成员变量和代码块优于构造方法执行。
至于更深层次为什么这个顺序,就要更深入了解 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 方法去读写。
- 继承:子类继承父类,即“子承父业”,子类拥有父类除私有的所有属性和方法,自己还能在此基础上拓展自己新的属性和方法。主要目的是复用代码。
- 多态:多态是同一个行为具有多个不同表现形式或形态的能力。即一个父类可能有若干子类,各子类实现父类方法有多种多样,调用父类方法时,父类引用变量指向不同子类实例而执行不同方法,这就是所谓父类方法是多态的。
最后送你一张图捋一捋其中的关系吧。
“好的,二哥,我来消化一下,今天内容真不少。你先去休息一下。”三妹回应到。
参考链接: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 即可免费领取。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/11332.html