Java基础-面向对象
参考文章:菜鸟教程-Java教程
参考文章:[廖雪峰-Java面向对象](面向对象基础 - 廖雪峰的官方网站 (liaoxuefeng.com))
1.Java 中的继承
Java 中类的字段的初始化顺序:==默认初始化/直接初始化——->构造函数初始化== && ==父类初始化——->子类初始化==
1 | public class ConstructionTest { |
需要注意的是 Java 不支持多继承,但支持多重继承。
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 继承提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
继承可以使用
extends
和implements
这两个关键字来实现继承,而且所有的类都是继承于java.lang.Object
,当一个类没有继承的两个关键字,则默认继承 Object(这个类在 java.lang 包中,所以不需要 import)祖先类。
子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。
如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。
1 | public class ExtendsTest { |
在 Java 中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
因此我们得出结论:==如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。==
这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
1 | public class SuperClass { |
注意:final关键字修饰类时表明该类不能被继承;final关键字修饰方法时表明该方法不能被重写;final关键字修饰实例字段时表明该字段初始化后不能被修改。
2.Java 中的重写与重载
区别点 | 重载方法 | 重写方法 |
---|---|---|
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常抛出 | 可以修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
访问控制 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
方法的重写(Overriding)和重载(Overloading)是 Java 多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
(1)方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
(2)方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
(3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
1 | public class OverrideTest { |
3.Java 中的多态
多态的概念:多态是同一个行为
具有多个不同表现形式
或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:
多态的三个必要条件:
- 继承
- 重写
- 父类引用指向子类对象:Parent p = new Child();
==当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。==
多态的好处:可以使程序有良好的扩展
,并可以对所有类的对象进行通用处理。
Java 中其实没有虚函数的概念,它的普通函数就相当于 C++ 的虚函数,动态绑定是 Java 的默认行为。如果 Java 中不希望某个函数具有虚函数特性,可以加上 final 关键字变成非虚函数。看看 Java 多么贴心啊~
4.Java 中的封装
封装的概念:在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程序代码更容易理解与维护,也加强了程序代码的安全性。
封装的优点:
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
实现 Java 封装的步骤
- 修改属性的可见性来限制对属性的访问(一般限制为private),例如:
1 | public class Person { |
这段代码中,将 name 和 age 属性设置为私有的,只能本类才能访问,其他类都访问不了,如此就对信息进行了隐藏。
- 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问,例如:
1 | public class Person{ |
采用 this 关键字是为了解决实例变量(private String name)和局部变量(setName(String name)中的name变量)之间发生的同名冲突
。
5.Java 中的枚举类
Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一年的 12 个月份,一个星期的 7 天,方向有东南西北等。
Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 , 来分割。
1 | enum Color |
执行以上代码输出结果为:
1 | RED |
==每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的==。以上的枚举类 Color 转化为类实现:
1 | class Color |
通过enum
定义的枚举类,和其他的class
有什么区别?
答案是没有任何区别。enum
定义的类型就是class
,只不过它有以下几个特点:
- 定义的
enum
类型总是继承自java.lang.Enum
,且无法被继承; - 只能定义出
enum
的实例,而无法通过new
操作符创建enum
的实例; - 定义的每个实例都是引用类型的唯一实例;
- 可以将
enum
类型用于switch
语句。
例如,我们定义的Color
枚举类:
1 | public enum Color { |
编译器编译出的class
大概就像这样:
1 | public final class Color extends Enum { // 继承自Enum,标记为final class |
所以,编译后的enum
类和普通class
并没有任何区别。但是我们自己无法按定义普通class
那样来定义enum
,必须使用enum
关键字,这是Java语法规定的。
enum 定义的枚举类默认继承了
java.lang.Enum
类,并实现了java.lang.Serializable
和java.lang.Comparable
两个接口。
values()
, ordinal()
和 valueOf()
方法位于 java.lang.Enum
类中:
- values() 返回枚举类中所有的值。
- ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
- valueOf()方法返回指定字符串值的枚举常量。
1 | enum Color |
枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。
1 | enum Color |
6.Java 中的记录类
记录类的由来
使用String
、Integer
等类型的时候,这些类型都是不变类,一个不变类具有以下特点:
- 定义class时使用
final
,无法派生子类; - 每个字段使用
final
,保证创建实例后无法修改任何字段。
假设我们希望定义一个Point
类,有x
、y
两个变量,同时它是一个不变类,可以这么写:
1 | public final class Point { |
为了保证不变类的比较,还需要正确覆写equals()
和hashCode()
方法,这样才能在集合类中正常使用。后续我们会详细讲解正确覆写equals()
和hashCode()
,这里演示Point
不变类的写法目的是,这些代码写起来都非常简单,但是很繁琐。
记录类的定义
从Java 14开始,引入了新的Record
类。我们定义Record
类时,使用关键字record
。把上述Point
类改写为Record
类,代码如下:
1 | public class Main { |
仔细观察Point
的定义:
1 | record Point(int x, int y) {} |
把上述定义改写为class,相当于以下代码:
1 | final class Point extends Record { |
除了用final
修饰class以及每个字段外,编译器还自动为我们创建了构造方法,和字段名同名的方法,以及覆写toString()
、equals()
和hashCode()
方法。
换句话说,使用record
关键字,可以一行写出一个不变类。
和enum
类似,我们自己不能直接从Record
派生,只能通过record
关键字由编译器实现继承。
记录类的构造方法
编译器默认按照record
声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。那么问题来了,如果我们要检查参数,应该怎么办?
假设Point
类的x
、y
不允许负数,我们就得给Point
的构造方法加上检查逻辑:
1 | public record Point(int x, int y) { |
注意到方法public Point {...}
被称为Compact Constructor,它的目的是让我们编写检查逻辑,编译器最终生成的构造方法如下:
1 | public final class Point extends Record { |
作为record
的Point
仍然可以添加静态方法。一种常用的静态方法是of()
方法,用来创建Point
:
1 | public record Point(int x, int y) { |
这样我们可以写出更简洁的代码:
1 | var z = Point.of(); |
7.Java 中的内部类
1.Inner Class
1 | public class Outer { |
上述定义的Outer
是一个普通类,而Inner
是一个Inner Class,它与普通类有个最大的不同,就是Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例。观察上述代码,要实例化一个Inner
,我们必须首先创建一个Outer
的实例,然后,调用Outer
实例的new
来创建Inner
实例。这是因为Inner Class除了有一个this
指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this
访问这个实例。所以,实例化一个Inner Class不能脱离Outer实例。
Inner Class和普通Class相比,除了能引用Outer实例外,还有一个额外的“特权”,就是可以修改Outer Class的private
字段,因为Inner Class的作用域在Outer Class内部,所以能访问Outer Class的private
字段和方法。
观察Java编译器编译后的
.class
文件可以发现,Outer
类被编译为Outer.class
,而Inner
类被编译为Outer$Inner.class
。(我本地的外部类名称为Outer1)
2.Anonymous Class
1 | public class Outer { |
观察asyncHello()
方法,我们在方法内部实例化了一个Runnable
。Runnable
本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable
接口的匿名类,并且通过new
实例化该匿名类,然后转型为Runnable
。在定义匿名类的时候就必须实例化它!匿名类和Inner Class一样,可以访问Outer Class的private
字段和方法。之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义Inner Class可以少写很多代码。
观察Java编译器编译后的
.class
文件可以发现,Outer
类被编译为Outer.class
,而匿名类被编译为Outer$1.class
。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer$1
、Outer$2
、Outer$3
……(我本地的外部类名称为Outer2)
3.Static Nested Class
1 | public class Outer { |
用static
修饰的内部类和Inner Class有很大的不同,它不再依附于Outer
的实例,而是一个完全独立的类,因此无法引用Outer.this
,但它可以访问Outer
的private
静态字段和静态方法。如果把StaticNested
移到Outer
之外,就失去了访问private
的权限。
观察Java编译器编译后的
.class
文件可以发现,Outer
类被编译为Outer.class
,而静态内部类被编译为Outer$Inner.class
。((我本地的外部类名称为Outer3,静态内部类名称为StaticNested))
4.小结
Java的内部类可分为Inner Class、Anonymous Class和Static Nested Class三种:
- Inner Class和Anonymous Class本质上是相同的,都必须依附于Outer Class的实例,即隐含地持有
Outer.this
实例,并拥有Outer Class的private
访问权限; - Static Nested Class是独立类,但拥有Outer Class的
private
静态成员访问权限。
8.Java 中的 classpath 和 jar
1.classpath
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。因为Java是编译型语言,源码文件是.java
,而编译后的.class
文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个abc.xyz.Hello
的类,应该去哪搜索对应的Hello.class
文件。所以,classpath
就是一组目录的集合,它设置的搜索路径与操作系统相关。例如,在Windows系统上,用;
分隔,带空格的目录用""
括起来,可能长这样:
1 | C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin" |
在Linux系统上,用:
分隔,可能长这样:
1 | /usr/shared:/usr/local/bin:/home/liaoxuefeng/bin |
现在我们假设classpath
是.;C:\work\project1\bin;C:\shared
,当JVM在加载abc.xyz.Hello
这个类时,会依次查找:
- <当前目录>\abc\xyz\Hello.class
- C:\work\project1\bin\abc\xyz\Hello.class
- C:\shared\abc\xyz\Hello.class
注意到.
代表当前目录。如果JVM在某个路径下找到了对应的class
文件,就不再往后继续搜索。如果所有路径下都没有找到,就报错。
classpath
的设定方法有两种:
在系统环境变量中设置
classpath
环境变量,不推荐;在启动JVM时设置
classpath
变量,推荐。
我们强烈不推荐在系统环境变量中设置classpath
,那样会污染整个系统环境。在启动JVM时设置classpath
才是推荐的做法。实际上就是给java
命令传入-classpath
或-cp
参数:
1 | java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello |
或者使用-cp
的简写:
1 | java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello |
没有设置系统环境变量,也没有传入-cp
参数,那么JVM默认的classpath
为.
,即当前目录:
1 | java abc.xyz.Hello |
上述命令告诉JVM只在当前目录搜索Hello.class
。
在IDE中运行Java程序,IDE自动传入的-cp
参数是当前工程的bin
目录和引入的jar包。(注意:这是针对Exclipse IDE,不同的IDE不同,如下)
通常,我们在自己编写的class
中,会引用Java核心库的class
,例如,String
、ArrayList
等。这些class
应该上哪去找?有很多“如何设置classpath”的文章会告诉你把JVM自带的rt.jar
放入classpath
,但事实上,根本不需要告诉JVM如何去Java核心库查找class
,JVM怎么可能笨到连自己的核心库在哪都不知道?
不要把任何Java核心库添加到classpath中!JVM根本不依赖classpath加载核心库!
更好的做法是,不要设置classpath
!默认的当前目录.
对于绝大多数情况都够用了。
2.jar
如果有很多.class
文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。
jar包就是用来干这个事的,它可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发给客户,就简单多了。
jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class
,就可以把jar包放到classpath
中:
1 | java -cp ./hello.jar abc.xyz.Hello |
这样JVM会自动在hello.jar
文件里去搜索某个类。
那么问题来了:如何创建jar包?
因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip
改为.jar
,一个jar包就创建成功。
假设编译输出的目录结构是这样:
1 | package_sample |
这里需要特别注意的是,jar包里的第一层目录,不能是bin
,而应该是hong
、ming
、mr
。如果在Windows的资源管理器中看,应该长这样:
如果长这样:
说明打包打得有问题,JVM仍然无法从jar包中查找正确的class
,原因是hong.Person
必须按hong/Person.class
存放,而不是bin/hong/Person.class
。
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
1 | java -jar hello.jar |
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。