Java_oop
类和对象
面向对象编程(OOP)
面向过程–自己掌控整个过程
面向对象–关注的是完成这件事的对象,而无需知道过程。
- 面向过程是流程化的,一步一步,上一步做完了,再做下一步。
- 面向对象是模块化的,我做我的,你做你的,我需要你做的话,我就告诉你一声。我不需要知道你到底怎么做,只看功劳不看苦劳。
类
我们之前学习过了cpp,所以对面向对象、类等这些知识点还是相对熟悉的。
创建java对象时,需要使用new关键字。创建类对象也需要
- 所有对象在创建的时候都会在堆内存中分配空间
- 创建对象时需要一个
main()方法作为入口,main()方法可以在当前类中,也可以在另一个类中。
例子:
1 | |
- 与cpp不同的是,java作为一门纯粹的面向对象语言,它强制要求所有代码(包括程序的入口点)都必须封装在类中。
如果万物皆对象,都需要先创建对象实例才能调用方法,那第一个main方法是怎么被调用的呢?
- 我们通过
static关键字解决!
1 | |
static 意味着这个方法是属于类本身的,而不是属于由这个类创建出来的某个具体对象。
实际开发中,我们通常不在当前类中直接创建对象并使用它,而是放在使用对象的类中。
1 | |
初始化类中变量的方法
方法一:直接通过引用变量来赋值。
1 | |
方法二:通过方法初始化
1 | |
方法三:通过构造方法初始化
直接把要初始化变量的值当作参数传入,并赋值
1 | |
在cpp中我们一般使用的就是方法3,java也是如此。
Object类
万物皆对象。万物指的是java中的所有类,这些类都是Object类的子类
对象比较
1 | |
用于返回对象的哈希码
按照约定,相等的对象必须具有相等的哈希码,如果重写了equals方法,就要重写hashCode方法。
1 | |
这样子生成,相等的对象就会有相同的哈希码。
1 | |
用于比较两个对象的内存地址是否相等。
1 | |
但我们一般会重写,修改成比较两个对象的值是否相等。
- 根据类里的对象来设置,一般是一个个比较
对象拷贝
默认实现只做浅拷贝,且类必须实现Cloneable接口。
Object 本身没有实现 Cloneable 接口,所以在不重写 clone 方法的情况下直接直接调用该方法会发生 CloneNotSupportedException 异常。
对象转字符串
public String toString():返回对象的字符串表示。默认实现返回类名@哈希码的十六进制表示,但通常会被重写以返回更有意义的信息。
1 | |
多线程调度
每个对象都可以调用Object 的 wait/notify 方法来实现等待/通知机制
反射
一开始不知道要初始化的类是什么,所以没办法用new关键字直接创造对象。
反射的缺点:
- 破坏封装
- 性能开销
反射的优点:
- 开发通用框架:像 Spring,为了保持通用性,通过配置文件来加载不同的对象,调用不同的方法。
- 动态代理:在面向切面编程中,需要拦截特定的方法,就会选择动态代理的方式,而动态代理的底层技术就是反射。
- 注解:注解本身只是起到一个标记符的作用,它需要利用反射机制,根据标记符去执行特定的行为。
public final native Class<?> getClass():用于获取对象的类信息,如类名。比如说:
1 | |
垃圾回收
protected void finalize() throws Throwable:当垃圾回收器决定回收对象占用的内存时调用此方法。
package包
java中我们使用package来解决名字冲突
- 一个类总是属于某个包,类名只是一个简写,真正的完整类名是
包名.类名
包可以是多层结构,用.隔开。例如:java.util。
[!NOTE]
要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
没有定义包名的class,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。
包的作用域
包作用域是指:当一个类、接口、成员没有显式地使用任何访问修饰符时,它所拥有的访问权限。
位于同一个包的类,可以访问包作用域的字段和方法。
不用public、protected、private修饰的字段和方法就是包作用域。
把一个类定义在包下:
1 | |
这样子就将Person和Main都定义在hello下了。
main可以直接访问person类,它们位于同一包作用域下。
导入包:
直接写出完整类名
1 | |
很麻烦
使用 import 语句
导入类后写简单类名。
1 | |
import时可以使用通用标识符,直接将这个包下的所有class都导入进来,但是不包含子包的class
1 | |
但是不推荐这种写法,导入多个包后,很难看出类属于哪个包
还有一种import static的语法,它可以导入一个类的静态字段和静态方法:
1 | |
没怎么见过这个方法
在代码中,当编译器遇到一个class名称时:
- 如果是完整类名,就直接根据完整类名查找这个
class; - 如果是简单类名,按下面的顺序依次查找:
- 查找当前
package是否存在这个class; - 查找
import的包是否包含这个class; - 查找
java.lang包是否包含这个class。
- 查找当前
这里我们知道,在编写class时,编译器会自动帮我们import:
- import当前package的其他class
import java.lang.*
如果有两个class名称相同,例如,mr.jun.Arrays和java.util.Arrays,那么只能import其中一个,另一个必须写完整类名。
变量
Java 变量就好像一个容器,可以保存程序在运行过程中的值,它在声明的时候会定义对应的数据类型(Java 分为两种数据类型:基本数据类型和引用数据类型)
变量按照作用域的范围又可分为三种类型:局部变量,成员变量和静态变量。
局部变量:
- 在栈上分配
- 没有默认值,被声明后必须经过初始化才可以使用
- 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,将会被销毁。
声明成员变量时的注意事项:
- 成员变量声明在一个类中,但在方法、构造方法和语句块之外。
- 当一个对象被实例化之后,每个成员变量的值就跟着确定。
- 成员变量在对象创建的时候创建,在对象被销毁的时候销毁。
- 成员变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息。
- 成员变量可以声明在使用前或者使用后。
- 访问修饰符可以修饰成员变量。
- 成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见;成员变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。
静态变量:
- 静态变量在类中以 static 关键字声明,但必须在方法构造方法和语句块之外。
- 无论一个类创建了多少个对象,类只拥有静态变量的一份拷贝。
- 静态变量除了被声明为常量外很少使用。
- 静态变量储存在静态存储区。
- 静态变量在程序开始时创建,在程序结束时销毁。
- 与成员变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型。
- 静态变量的默认值和实例变量相似。
- 静态变量还可以在静态语句块中初始化。
常量:
- 代表常数,便于修改(例如:圆周率的值,
final double PI = 3.14) - 增强程序的可读性(例如:常量 UP、DOWN 用来代表上和下,
final int UP = 0)
java要求常量名必须大写
方法
只有方法被调用时,它才会执行。Java 中最有名的方法当属 main() 方法,这是程序的入口。
如何声明方法
- 方法的声明反映了方法的可见性(public、private、protected、default)、返回类型(int、void…)、方法名和参数

访问权限: - public:该方法可以被所有类访问。
- private:该方法只能在定义它的类中访问。
- protected:该方法可以被同一个包中的类,或者不同包中的子类访问。
- default:如果一个方法没有使用任何访问权限修饰符,那么它是 package-private 的,意味着该方法只能被同一个包中的类可见。
方法的种类
- 标准类库方法
- 用户自定义方法
预先定义方法
Java 提供了大量预先定义好的方法供我们调用,也称为标准类库方法,或者内置方法。
用户自定义方法
当一个方法被 static 关键字修饰时,它就是一个静态方法。换句话说,静态方法是属于类的,不属于类实例的(不需要通过 new 关键字创建对象来调用,直接通过类名就可以调用)
实例方法
- 实例方法:没有static关键字修饰的,但是在类中有声明的方法。
- 调用这种方法之前,必须创建类的对象
1 | |
实例方法有两种特殊类型:
- getter 方法
- setter 方法
getter 方法用来获取私有变量(private 修饰的字段)的值,setter 方法用来设置私有变量的值。
1 | |
getter 方法以 get 开头,setter 方法以 set 开头。
静态方法
有 static 关键字修饰的方法就叫做静态方法。
StaticMethodExample 类中,mian 和 add 方法都是静态方法,不同的是,main 方法是程序的入口。当我们调用静态方法的时候,就不需要 new 出来类的对象,就可以直接调用静态方法了,一些工具类的方法都是静态方法,比如说 hutool 工具类库,里面有大量的静态方法可以直接调用。
抽象方法
没有方法体的方法被称为抽象方法,它总是在抽象类中声明。这意味着如果类有抽象方法的话,这个类就必须是抽象的。可以使用 abstract 关键字创建抽象方法和抽象类。
1 | |
一个类继承抽象类之后,必须重写抽象方法
可变参数
允许方法接受零个或多个指定类型的参数。它使用省略号(...)表示,实际上是一个语法糖,底层仍然是数组。
- 尽量不要使用可变参数,如果要用的话,可变参数需要在列表最后一位
- 当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法。
native本地方法
JNI:Java Native Interface
一般情况下,我们完全可以使用 Java 语言编写程序,但某些情况下,Java 可能满足不了需求,或者不能更好的满足需求,比如:
- ①、标准的 Java 类库不支持。
- ②、我们已经用另一种语言,比如说 C/C++ 编写了一个类库,如何用 Java 代码调用呢?
- ③、某些运行次数特别多的方法,为了加快性能,需要用更接近硬件的语言(比如汇编)编写。
JNI 的缺点:
- ①、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。
- ②、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了 Java 和 C/C++ 之间的耦合性。
手把手教你用 C语言实现 Java native 本地方法 | 二哥的Java进阶之路
native 用来修饰方法,用 native 声明的方法表示该方法的实现在外部定义,可以用任何语言去实现它,比如说 C/C++。 简单地讲,一个 native Method 就是一个 Java 调用非 Java 代码的接口。
native 语法:
- ①、修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。
- ②、不能用 abstract 修饰,也没有方法体,也没有左右大括号。
- ③、返回值可以是任意类型
“三妹,你学的不错嘛。”我对三妹的学习能力感到非常的欣慰,“我们在日常编程中看到 native 修饰的方法,只需要知道这个方法的作用是什么,至于别的就不用管了,操作系统会给我们实现,初学的时候也不需要太过深入。”
构造方法
Java 有两种类型的构造方法:无参构造方法和有参构造方法。
创建构造方法的规则
- 构造方法的名字必须和类名一样;
- 构造方法没有返回类型,包括 void;
- 构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。
构造方法不能被子类继承,所以用final和abstract关键字修饰没有也意义
构造方法用于初始化一个对象,所以用static关键字修饰没有意义
多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 关键字修饰没有必要。
1 | |
如果写成了 void Demo() ,这并不是构造方法,而是会看作普通方法
默认构造方法
如果一个构造方法中没有任何参数,那么它就是一个默认构造方法,也称为无参构造方法。
无参构造方法是可以缺省的,我们开发者并不需要显式的声明无参构造方法
有参构造方法
有参构造方法可以为不同的对象提供不同的值。当然,也可以提供相同的值。
重载构造方法
- 可以用于应对不同的对象
1 | |
构造方法主要是用于初始化对象的字段,并没有返回类型
构造方法的调用是隐式的,通过编译器完成
复制对象
复制一个对象可以通过下面三种方式完成:
- 通过构造方法
- 通过对象的值
- 通过 Object 类的
clone()方法
通过构造方法
1 | |
这里直接将对象p1作为参数传递给构造函数。(我们在前面重载了这个构造方法,使得可以通过这个方法来实现。
通过对象的值
很粗暴,直接一个个赋值
通过Object类的 clone() 方法
1 | |
通过 clone() 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 clone() 方法,然后再调用 clone() 方法(ClonePerson p2 = (ClonePerson) p1.clone())
- 但这里应该是浅拷贝
访问权限修饰符
在 Java 中,提供了四种访问权限控制:
- 默认访问权限(包访问权限)
- public
- private
- protected
和cpp几乎一模一样
代码初始化块
代码初始化块用于初始化一些成员变量
抽象类
定义抽象类
定义的时候需要用到关键字 abstract 。
1 | |
抽象类的特征
抽象类不可以实例化(即不能new)
但可以有子类,子类通过 extends 关键字来继承抽象类。
1 | |
如果一个类定义了一个或多个抽象方法,那这个类必须是抽象类。也就是说,只要类中定义了抽象方法,就一定是抽象类。
- 普通类不可以定义抽象方法
- 抽象类中可以定义抽象方法也可以定义普通方法。抽象方法没有方法体。
抽象类派生的子类必须实现父类中定义的抽象方法。(除非子类也是抽象类)
抽象类的应用场景
第一种场景:代码复用
- 共享通用代码,避免重复代码。
比如抽象类中定义了普通方法(有方法体),例如sleep()方法中打印是个人都要睡觉。这样子子类可以直接继承复用。
第二种场景:子类实现API
抽象类中给出动物叫的API,子类继承时填写方法体。
比如之前我们在cpp学习过程中写过的动物叫,小狗就是汪汪叫,小猫就是喵喵叫等。
接口
- java中可以通过两种形式来达到抽象的目的,一个是抽象类,一个是接口
定义接口
接口通过 interface 关键字来定义,它可以包含一些常量和方法。
例子:
1 | |
其反编译后的字节码:
1 | |
可以看到接口中的所有变量或方法都会自动加上 public 关键字
接口中定义的变量会在编译的时候自动加上
public static final修饰符。也就是说变量在接口里就是一个常量。
Every field declaration in the body of an interface is implicitly public, static, and final.没有使用
privatedefault或者static关键字修饰的方法是隐式抽象的。在编译的时候会自动加上public abstract修饰符。java8开始,接口中允许有静态方法
default方法就是接口中定义的、有具体实现的方法。它允许接口在不破坏现有实现的情况下,添加新的方法。
允许在接口中定义默认方法的理由很充分,因为一个接口可能有多个实现类,这些类就必须实现接口中定义的抽象类,否则编译器就会报错。假如我们需要在所有的实现类中追加某个具体的方法,在没有default方法的帮助下,我们就必须挨个对实现类进行修改。
一些特性
- 接口不允许直接实例化。需要定义一个类去实现接口。
例子:
1 | |
这里使用了Computer类来实现Electronic接口
再进行实例化:
1 | |
- 接口可以为空,既可以不定义变量,也可以不定义方法。
- 不要在定义接口的时候使用final关键字。因为接口就是为了让子类实现的,而 final 阻止了这种行为。
- 接口的抽象方法不能是private,protected或者final
- 接口的变量是隐式的public static final,值无法变
接口的作用
使某些类具有我们想要的功能。实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。
- Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。
Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。

实现多态
- 1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。
- 2、子类要重写父类的方法,Circle 和 Square 都重写了
name()方法。 - 3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。
内部类
- 在类里定义类
- 成员内部类、局部内部类、匿名内部类和静态内部类
成员内部类:在类里嵌套一个类。
1 | |
成员内部类可以访问外部类的所有成员属性。
外部类要访问内部类需要先创建一个成员内部类对象,再通过对象来访问。
如果要在静态方法里访问成员内部类,需要先创建外部类,再创建外部类的成员内部类对象,进而访问成员内部类的成员。
局部内部类:在方法或者作用域里定义的类
局部内部类的生命周期仅限于作用域内。
1 | |
匿名内部类
- 在创建对象的同时定义类体,没有显式的类名。通常用于实现接口或继承类
1 | |
这里:
1 | |
- 不能有构造器也不能是抽象类。
- 访问外部变量需要final

静态内部类
- 静态内部类:用static关键词修饰的定义在类里的类。
不允许访问外部类中非static的变量和方法。
使用内部类还能够为我们带来如下特性:
- 1、内部类可以使用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
- 2、在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
- 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
- 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。
封装继承多态
封装和cpp几乎一模一样
继承
java的继承用到的关键字是 extends
- java中的继承是单一继承,一个子类只能拥有一个父类,所以extends只能继承一个类。
1 | |
子类继承父类后,就拥有父类的非私有的属性和方法。
implements 关键字:可以变相使java拥有多继承的特性,使用范围为类实现接口的情况,一个类可以实现多个接口
1 | |
(接口中的方法不能具体定义,只能声明,所以继承接口必须重写接口内的方法)
例子:
1 | |
这里就相当于继承了多个类
this 和 super
this 表示当前对象,是指向自己的引用。
1 | |
super 表示父类对象,是指向父类的引用。
1 | |
子类的构造过程必须调用其父类的构造方法: Java 虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加。
- 如果子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。
方法重写和方法重载
方法重写就是把方法重新实现一次
方法重载是方法名相同,参数不一致。调用时会根据传递的参数进行选择。
向上转型
通过子类对象(小范围)实例化父类对象(大范围),也就是将子类对象赋值给父类类型的引用变量。
- 自动转换

父类引用变量指向子类对象后,只能使用父类已声明的方法,如果方法被重写,就会执行子类的方法。
向下转型
通过父类对象实例化子类对象(将父类对象赋值给子类对象)
- 需要手动强制转换

在 Java 继承中,父子类初始化先后顺序为:
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造方法
- 子类中普通成员变量和代码块,子类的构造方法
多态
Java 的多态是指在面向对象编程中,同一个类的对象在不同情况下表现出来的不同行为和状态。
- 子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
- 子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
- 可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。
多态和重载的区别
多态是动态多态,发生在运行时。通过继承和方法重写的方式实现。
重载是静态多态,发生在编译时。是同一类中的同名方法。
1 | |
多态是通过向上转型实现的,让不同类型的对象可以对同一方法调用做出不同的响应。
static关键字
- 方便在没有创建对象的情况下进行调用。属于类的方法,而不是对象的方法。
- 通过
类名.方法名来调用
静态变量:被整个类共用的变量。
如果是普通变量,是每个对象都有一个变量。
静态方法有以下这些特征。
- 静态方法属于这个类而不是这个类的对象;
- 调用静态方法的时候不需要创建这个类的对象;
- 静态方法可以访问静态变量。
instanceof 关键字
用于判断对象是否符合指定的类型。
我们使用 instanceof 比较的目的,也就是希望如果结果为 true 的时候能进行类型转换。
不可变类
1)确保类是 final 的,不允许被其他类继承*。
2)确保所有的成员变量(字段)是 final 的,这样的话,它们就只能在构造方法中初始化值,并且不会在随后被修改。
3)不要提供任何 setter 方法。
4)如果要修改类的状态,必须返回一个新的对象。
Java注解
注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响,由编译器决定该执行哪些操作。
注解的生命周期
- SOURCE:在源文件中有效,被编译器丢弃。
- CLASS:在编译器生成字节码文件中有效,在运行时会被处理类文件的jvm丢弃
- RUNTIME:在运行时有效。允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。
截止到 Java 9,注解的类型一共有 11 种,定义在 ElementType 枚举中。
1)TYPE:用于类、接口、注解、枚举
2)FIELD:用于字段(类的成员变量),或者枚举常量
3)METHOD:用于方法
4)PARAMETER:用于普通方法或者构造方法的参数
5)CONSTRUCTOR:用于构造方法
6)LOCAL_VARIABLE:用于变量
7)ANNOTATION_TYPE:用于注解
8)PACKAGE:用于包
9)TYPE_PARAMETER:用于泛型参数
10)TYPE_USE:用于声明语句、泛型或者强制转换语句中的类型
11)MODULE:用于模块