面向对象是最重要的一章,java是一门纯面向对象编程语言, 我们后面写的所有程序都是在面向对象的基础上编写的。面向对象难在思想和语法上,真正应用的时候会不知不觉的渗透在你的代码里,好多程序员写程序会写, 但是为什么这样写? 不知道,所以本章很多都是理解和需要思考的东西.
在本章你要学的重点知识点:

  1. 类与对象
  2. 构造方法
  3. 访问权限
  4. 继承
  5. 多态 ( 最重要 )
  6. 抽象和接口
  7. 内存分析

其他知识点不是说不重要. 相对而言. 最重要的是上面这几个。
面向对象和面向过程:
讲面向对象之前, 还要再提两个概念, 毕竟没有对比就没有伤害:
面向过程:
从名字上可以看出来, 编程的思路是按照事务的发展流程而编写的,最典型的例子就是把大象装冰箱总共分几步? 第一步, 把冰箱门打开,
第二步, 把大象装进去,
第三步.把冰箱门关上。
优点: 思路简单, 写起来也简单
缺点: 维护困难,代码量越大, 越难维护。
面向对象:
一切以对象为中心,我们扮演的是上帝的角色。比如, 同样是大象进冰箱, 用面向对象的思维来解决的话就是, 创建一个大象, 然后告诉大象, 进冰箱里面去,具体怎么进冰箱由大象来完成。面向对象的思维可以把”我”的工作减少到最低,由对象来完成具体的操作。
优点: 超强的可扩展性,可维护性。
缺点: 上手比较难, 尤其是刚开始就接触面向对象。

类的组成

定义一个类的基本格式:

  1. [修饰符] class 类名{
  2. 0到多个构造器
  3. 0到多个成员变量
  4. 0到多个方法
  5. 0到多给初始化块
  6. }

修饰符可以写public final abstract或者不写,java类名要用大驼峰写法(PrimaryStu首字母大写的驼峰)。
一个java源文件(即文件后缀名为.java的文件)可以写多个类,但是里面只能有一个用public修饰的class。

构造器函数

构造器也叫构造方法或者构造函数,构造器与类名相同,没有返回值,连void都不能写;
构造器定义格式:

  1. [修饰符]与类名相同的名(形参列表){
  2. 构造器方法体代码
  3. }
  • 构造器函数名称与类名相同,没有返回值,不能写void 。
  • 如果类中没有手动添加构造器,编译器会默认添加一个无参构造器 。
  • 如果手动添加了构造器(无论什么形式),默认构造器就会消失,因为构造器可以重载。

    成员变量

    成员变量是定义在类中,方法体之外的变量(即类变量+实例变量)。实例变量在创建对象的时候实例化;
    成员变量可以被类中方法、构造方法和特定类的语句块访问。
    修饰符(public–protected–private)三选一、static、final,使用static修饰就是静态变量了(类变量)

  • 类变量:独立于方法之外的变量,用 static 修饰。

  • 局部变量:类的方法中的变量。
  • 实例变量(全局变量):独立于方法之外的变量,不过没有 static 修饰。

    1. public class Variable{
    2. static int allClicks=0; // 类变量
    3. String str="hello world"; // 实例变量
    4. public void method(){
    5. int i =0; // 局部变量
    6. }
    7. }

    局部变量

  • 局部变量声明在方法、构造方法或者语句块中;

  • 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁;
  • 访问修饰符不能用于局部变量;
  • 局部变量只在声明它的方法、构造方法或者语句块中可见;
  • 局部变量是在栈上分配的。
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。

    实例变量

  • 实例变量声明在一个类中,但在方法、构造方法和语句块之外;

  • 当一个对象被实例化之后,每个实例变量的值就跟着确定;
  • 实例变量在对象创建的时候创建,在对象被销毁的时候销毁;
  • 实例变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息;
  • 实例变量可以声明在使用前或者使用后;
  • 访问修饰符可以修饰实例变量;
  • 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见;
  • 实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。变量的值可以在声明时指定,也可以在构造方法中指定;实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName。

    类变量

  • 类变量也称为静态变量,在类中以static关键字声明,但必须在方法构造方法和语句块之外。

  • 无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。
  • 静态变量除了被声明为常量外很少使用。常量是指声明为public/private,final和static类型的变量。常量初始化后不可改变。
  • 静态变量储存在静态存储区。经常被声明为常量,很少单独使用static声明变量。
  • 静态变量在程序开始时创建,在程序结束时销毁。
  • 与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为public类型。
  • 默认值和实例变量相似。数值型变量默认值是0,布尔型默认值是false,引用类型默认值是null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。
  • 静态变量可以通过:ClassName.VariableName的方式访问。
  • 类变量被声明为public static final类型时,类变量名称一般建议使用大写字母。如果静态变量不是public和final类型,其命名方式与实例变量以及局部变量的命名方式一致。

    方法

    Java方法是语句的集合,它们在一起执行一个功能。
    方法是解决一类问题的步骤的有序组合 方法包含于类或对象中,方法在类中被创建,在其他地方被引用
    1. void printf(){
    2. System.out.println(111);
    3. }
    上面是一个无参的返回值为空的方法,参数可以加,返回值也可以是int型也可以是别的类型。
    方法的定义:
    1. [修饰符]方法的返回值类型 方法名称(形参列表){
    2. //方法体代码
    3. [return 返回值;]
    4. }
    方法的修饰符(public protected private)三选一、static、final、synchronize、native,使用static就是静态方法了

    代码块

    初始化代码块:将一些变量、语句(如打印语句)放到一个{},主是用来初始化一些值的,它的优先级比构造函数要高,一些需要多次使用但又是固定的值可以放进初始化块中。
    使用static就是静态初始化块了,静态初始化块优先级最高,多次实例化时只执行一次

    一、类与对象

    类是对具体事务的描述,对某一类事务的总结,对事物的归类。比如车是一个大类包括轿车 suv mpv 房车等等。
    对象是什么? 对象就是具体的一辆车比如你购买的一辆宝马X6,就是具体要执行操作的事务。

    定义类

    1. public class Car { }
    我们对车要进行描述,描述车需要有两个标准去描述, 1. 车有什么属性, 2. 车能干什么.
    属性: 使用成员变量来描述属性
    动作: 使用成员方法来描述动作
    成员变量是什么? 很简单, 之前不是学过变量么. 我们之前的变量是写在main方法里的. 这里的成员变量指的是直接写在类里的变量叫成员变量. 比如. 车有颜色, 车有品牌, 车有排量等等。
    成员方法是什么? 就是我们之前学习的方法. 把前面的static去掉就是成员方法. 关于static后面我们会讲,这里不用纠结,直接干掉就行。好了, 车能执行哪些动作? 跑, 跳高, 游泳等等。
    1. public class Car {
    2. //成员变量. 可以初始化,也可以只声明不初始化
    3. String color; // 汽车有颜色
    4. String brand = "奔驰"; // 汽车有品牌, 所有车都是奔驰
    5. String displacement; // 汽车有排量
    6. //方法
    7. public void run(){
    8. System.out.println("破车会跑");
    9. }
    10. public void jump(){
    11. System.out.println("神车~~ 会跳高");
    12. }
    13. public void swim(){
    14. System.out.println("我的车会游泳");
    15. }
    16. }

    类实例化对象

    1. public static void main(String[] args) {
    2. Car c = new Car(); // 创建对象
    3. }

    Car c =new Car(); 这句话就是传说中的创建对象了. 注意, 这里其实是一个赋值操作, 那根据赋值操作的特点. 肯定要先执行等号右边的代码, 然后赋值给等号左边. 我们挨个分析. new Car(); new表示创建, 新建. 后面跟的是类名+括号. 整个这句话你可以理解为 创建一个车类型的对象. 那这个车是根据Car类创建的. 所以这个对象一定符合类的定义. 这个很好理解. 用你的图纸造的车. 肯定符合你图纸上设计的样子. Car c 这是个啥? 对比一下你瞬间秒懂 String a …… 没错. 就是声明了一个变量. c是Car类型的. 只不过在面向对象的世界里变量是没有市场的. 大家都叫它引用. 这也就是我们讲的第二大数据类型, 引用数据类型. 说的就是咱们写的类. 声明出来的变量. 整体合起来就是: 创建一个车类的对象. 然后赋值给变量c. 以后变量c就是使用的这辆车.

OK, 对象创建出来了. 接下来. 怎么用呢?

  1. Car c = new Car();
  2. c.color = "红色"; // 汽车的颜色设置成红色
  3. c.displacement = "3.6T"; // 排量
  4. //c.seat = 5; // 报错. 在类中你没有写这个属性.
  5. System.out.println(c.color);
  6. System.out.println(c.brand);
  7. System.out.println(c.displacement);
  8. c.run();
  9. c.jump();
  10. c.swim();

类里没有的属性你不能乱用.
总结: 类其实就是对某一类事物的归类和描述. 对象是通过类创建的,类是抽象概念, 对象是具体。

二、this关键字

this是啥? 我们看一个例子

  1. public class Car {
  2. String color;
  3. int seat;
  4. String displacement;
  5. public void run(){
  6. System.out.println(color + "颜色的车在跑");
  7. }
  8. public static void main(String[] args) {
  9. Car c = new Car();
  10. c.color = "红色";
  11. c.seat = 5;
  12. c.displacement = "1.6";
  13. c.run();
  14. }
  15. }

这里注意. 在run方法里我们使用了一次color变量,此时我们发现可以正常使用.
我们可以发现, 此时使用的color是对象里的color, 那如果我给run()传递一个参数也叫color呢?

  1. public class Car {
  2. String color;
  3. int seat;
  4. String displacement;
  5. public void run(String color){
  6. System.out.println(color + "颜色的车在跑"); //绿色颜色的车在跑
  7. System.out.println(this.color + "颜色的车在跑"); //红色颜色的车在跑
  8. }
  9. public static void main(String[] args) {
  10. Car c = new Car();
  11. c.color = "红色";
  12. c.seat = 5;
  13. c.displacement = "1.6";
  14. c.run("绿色");
  15. }
  16. }

方法查找变量的顺序: 方法内部(局部) > 对象
说白了就是就近原则, 那此时我即想用局部变量又想用对象中的变量,怎么进行区分呢? 此时就需要用到this关键字.

在java中, this表示当前类的对象 啥叫当前类的对象,就是正在执行这个方法的对象. c.run() => 在run中this就是c

总结:this可以帮我们在类中获取到对象的任何信息。如果没有命名冲突,可以省略this,但我们不推荐省略。

this()与super()使用详解

参考:https://blog.csdn.net/lncsdn_123/article/details/79025525
https://www.cnblogs.com/hasse/p/5023392.html

三、构造方法

构造的意义就是可以帮我们在创建对象的时候给对象传递一些信息.
构造方法的语法:

public 类名(参数….){ }

注意: 构造方法的名字必须和类名一致.

  1. public class Car{
  2. String name;
  3. String color;
  4. int seat;
  5. public Car(String name, String color, int seat){
  6. this.name = name; // 相当于p.name = name
  7. this.color = color; // 相当于p.color = color
  8. this.seat = seat; // 相当于p.seat = seat
  9. System.out.println("我就是一个可怜的构造方法");
  10. }
  11. public static void main(String[] args){
  12. Car p = new Car("大黄蜂", "绿色", 5); // 创建对象的时候. 后面这个小括号其实就是在调用构造方法
  13. System.out.println(p.name);
  14. System.out.println(p.color);
  15. System.out.println(p.seat);
  16. }
  17. }

总结: 构造方法存在的意义就是在对象在创建的时候给对象设置一些属性. 注意: 每个类都会有构造方法,如果不写, java会自动创建一个没有参数的构造方法. 但是, 自己写了构造方法了就覆盖掉无参的构造方法

构造方法的重载

想这么一个事儿, 我们可以在创建对象的时候给对象传递一些信息, 通常都是在构造方法里设置一些属性. 那如果现在我写一个”大侠”类, 里面会有很多个属性,

  1. public class DaXia{
  2. String name;
  3. String waihao;
  4. int age;
  5. String bangPai;
  6. }
  1. 好了, 接下来我们去创建大侠, 比如, 创建一个"岳不群", 那在创建岳不群的时候, 需要给出: 名字, 年龄, 帮派.

外号和口头禅对于岳不群而言都不需要,那如果是创建一个武松呢? 需要给出: 名字, 年龄, 帮派,外号. 这就会产生一个分歧,有些大侠有外号, 有些大侠没有外号,并且, 我们知道在创建对象的时候,我们是默认调用构造方法的. 那就需要我们写两个构造方法来满足两种不同的大侠,但是构造方法的名字还必须是类名,也就意味着, 我们要写两个名字相同的方法。在上一章里学过, 方法的名字相同参数的个数或者类型不同, 叫方法的重载。没错, 这里需要我们重载构造方法。

  1. public class DaXia{
  2. String name;
  3. String waihao;
  4. int age;
  5. String bangPai;
  6. public DaXia(String name, int age, String bangPai){
  7. this.name = name;
  8. this.age = age;
  9. this.bangPai = bangPai;
  10. }
  11. public DaXia(String name, int age, String bangPai, String waihao){
  12. this.name = name;
  13. this.age = age;
  14. this.bangPai = bangPai;
  15. this.waihao = waihao;
  16. }
  17. }

聪明的你应该又发现一个问题,两个构造方法的写法太像了. 能不能简化一下呢? OK. 没问题. 我们还可以使用this来调用当前类中的其他构造方法

  1. public class DaXia{
  2. String name;
  3. String waihao;
  4. int age;
  5. String bangPai;
  6. public DaXia(String name, int age, String bangPai){
  7. this.name = name;
  8. this.age = age;
  9. this.bangPai = bangPai;
  10. }
  11. public DaXia(String name, int age, String bangPai, String waihao){
  12. this(name, age, bangPai); // 调用自己类中的其他构造方法
  13. this.waihao = waihao;
  14. }
  15. }

this的两个作用:

  1. 表示当前类的对象, 可以访问成员变量和成员方法
  2. 可以调用当前类中的其他构造方法(重载的构造方法名都是与类名一致,通过传参不同调不同的构造方法)

小练习

1.用面向对象的思维来模拟LOL里的盖伦上阵杀敌
2.植物大战僵尸

植物类ZhiWu:包含name hp attack字段,和fight方法 僵尸类JiangShi:包含name hp attack字段,和eat方法 程序入口类Client:创建植物对象,僵尸对象,调用zw.fight() 调用js.eat()

ZhiWu.java

  1. public class ZhiWu {
  2. String name;
  3. int hp; //血量
  4. int attack; //攻击力
  5. //构造方法
  6. public ZhiWu(String name,int hp,int attack){
  7. this.name = name;
  8. this.hp = hp;
  9. this.attack = attack;
  10. }
  11. //植物打僵尸
  12. public void fight(JiangShi js){
  13. System.out.println("植物"+this.name+"正在打僵尸"+js.name);
  14. //僵尸血量减少
  15. js.hp -= this.attack;
  16. System.out.println("僵尸的血量剩余:"+js.hp);
  17. }
  18. }

JiangShi.java

  1. public class JiangShi {
  2. String name;
  3. int hp; //血量
  4. int attack; //攻击力
  5. //构造方法
  6. public JiangShi(String name,int hp,int attack){
  7. this.name = name;
  8. this.hp = hp;
  9. this.attack = attack;
  10. }
  11. //僵尸吃植物
  12. public void eat(ZhiWu zw){
  13. System.out.println("僵尸"+this.name+"正在吃植物"+zw.name);
  14. //植物血量减少
  15. zw.hp -= this.attack;
  16. System.out.println("植物的血量剩余:"+zw.hp);
  17. }
  18. }

Client.java

  1. public class Client {
  2. public static void main(String[] args) {
  3. ZhiWu zw = new ZhiWu("豌豆射手",1000,5);
  4. JiangShi js = new JiangShi("铁头僵尸",800,10);
  5. //植物打僵尸
  6. zw.fight(js);
  7. //僵尸吃植物
  8. js.eat(zw);
  9. }
  10. }

四、static静态字段和静态方法

假设, 我们现在回到清朝. 给清朝人上户口. 那此时就需要写一个类来装关于清朝人的信息

  1. public class Person{
  2. String name;
  3. String country;
  4. String address;
  5. public Person(String name, String country, String address){
  6. this.name = name;
  7. this.country = country;
  8. this.address = address;
  9. }
  10. }

接下来, 创建2个人

  1. public static void main(String[] args){
  2. Person p1 = new Person("李大猛", "大清", "北京珠市口八大胡同");
  3. Person p2 = new Person("花花", "大清", "北京朝阳门外");
  4. }

OK. 很easy. 但是, 我们想想啊. 大清亡了. 改成民国了. 那这时候程序怎么办呢?

  1. public static void main(String[] args){
  2. Person p1 = new Person("李大猛", "大清", "北京珠市口八大胡同");
  3. Person p2 = new Person("花花", "大清", "北京朝阳门外");
  4. p1.country = "民国";
  5. p2.country = "民国";
  6. }

是不是每个人的country属性都要改一下. 为什么呢?
两个对象分别是两块独立的内存区域,里面的内容也都是独立的,所以必然要改两次。但是, 你要知道,我国人民众多啊,这得改到什么时候去。那如果能把country这一项作为共享的数据,所有的对象都共享那是不是改起来就容易了,也就是说. 想办法变成这样
这样改一份就OK了. 那怎么才能让country变成共享的呢? 就是咱今天要学的static.
static表示静态,被static修饰的变量会被所有的对象共享, 并且在内存里只会保留一份.

  1. public class StaticTest {
  2. public String name;
  3. public int age;
  4. public static String country = "china";
  5. {
  6. System.out.println("这里是普通初始化块");
  7. }
  8. static {
  9. System.out.println("这里是静态初始化块");
  10. }
  11. public StaticTest(String name,int age){
  12. this.name = name;
  13. this.age = age;
  14. System.out.println("这里是构造方法");
  15. }
  16. public static void MyStaticFunc(){
  17. System.out.println("我是静态方法MyStaticFunc,国家="+StaticTest.country);
  18. }
  19. public void ShowNameFunc(){
  20. System.out.println("我是方法ShowNameFunc,name="+this.name+" ,age="+this.age);
  21. }
  22. public static void main(String[] args) {
  23. StaticTest obj1 = new StaticTest("zhangsan",18);
  24. StaticTest.MyStaticFunc(); //静态方法通过类调用
  25. //this.ShowNameFunc(); //main主函数也是一个静态方法,不能使用this
  26. obj1.ShowNameFunc();
  27. }
  28. }
  29. //打印结果
  30. 这里是静态初始化块
  31. 这里是普通初始化块
  32. 这里是构造方法
  33. 我是静态方法MyStaticFunc,国家=china
  34. 我是方法ShowNameFuncname=zhangsan ,age=18

类的执行顺序:
image.png
总结:静态字段/方法属于类,p1 p2是类Person创建的实例,实例对象可以访问类的静态字段/方法。静态字段/方法是在类实例化之前创建( this==实例),因此静态方法中不能使用this 且不能调用非静态方法(类中没有加static的方法)

  1. public class StaticTest {
  2. public String name;
  3. public int age;
  4. public static String country = "china";
  5. {
  6. System.out.println("这里是普通初始化块");
  7. }
  8. static {
  9. System.out.println("这里是静态初始化块");
  10. }
  11. public StaticTest(String name,int age){
  12. this.name = name;
  13. this.age = age;
  14. System.out.println("这里是构造方法");
  15. }
  16. public static void MyStaticFunc(){
  17. System.out.println("我是静态方法MyStaticFunc,国家="+StaticTest.country);
  18. }
  19. public void ShowNameFunc(){
  20. System.out.println("我是方法ShowNameFunc,name="+this.name+" ,age="+this.age);
  21. }
  22. public static void main(String[] args) {
  23. StaticTest obj1 = new StaticTest("zhangsan",18);
  24. StaticTest.MyStaticFunc(); //静态方法通过类调用
  25. // this.ShowNameFunc(); //main主函数也是一个静态方法,不能使用this
  26. obj1.ShowNameFunc();
  27. Person p1 = new Person("李大猛", "北京珠市口八大胡同");
  28. Person p2 = new Person("花花", "北京朝阳门外");
  29. Person.country = "民国";
  30. System.out.println(p1.country);
  31. System.out.println(p2.country);
  32. Person.setCountry("中华人民共和国");
  33. System.out.println(p1.country);
  34. System.out.println(p2.country);
  35. }
  36. }
  37. class Person {
  38. static String country;
  39. String name;
  40. String address;
  41. public static void setCountry(String country){
  42. // System.out.println(this.name); //静态方法中不能使用对象,即不能用this
  43. System.out.println("修改之前的静态字段Person.country="+Person.country); //可以使用静态字段
  44. Person.country = country;
  45. }
  46. //构造方法
  47. public Person(String name, String address) {
  48. this.name = name;
  49. this.address = address;
  50. }
  51. }

五、包和导包

https://www.liaoxuefeng.com/wiki/1252599548343744/1260467032946976
随着代码越写越多. 咱们不可能一直这样在src里创建java文件了,就好比你看片你不可能把所有的电影都堆桌面, 对吧, 你肯定要准备几个文件夹, 然后对这些片片进行分类, 哪些好看, 哪些无码, 哪些重口味~~, 一样的咱们的代码也是啊,不可能就这么堆src里,时间长了不好管理啊
windows操作系统使用文件夹来装不同的文件,在java里使用包来管理不同的java文件
怎么创建包? 看着
2、Java面向对象 - 图3
右键-> 新建-> package
2、Java面向对象 - 图4

注意: 包名一般用公司域名的翻转. 一般都是com或者org开头. 还有一些公司会用net开头,然后就是项目名, 最后一般都是功能模块名. 比如 你先在写的是qq的聊天窗口那就可以: com.qq.talk

创建出来的包是这样个样子的. 但是如果你去文件系统里看. 它是这样的.
2、Java面向对象 - 图5
所谓的”.”其实就是文件夹.
2、Java面向对象 - 图6
OK. 接下来我们到包里创建一个java文件看看
2、Java面向对象 - 图7
我们发现, 现在写的代码的第一行多了这样一句话叫package com.xyq.bao; package表示当前文件所属的包.
package需要注意的点:

  1. 必须放在有效代码的第一行. 不可以写在别处.
  2. package 后面的代码必须和文件系统的路径一致.

一个包OK了. 那如果是多个包呢? 我们到src位置创建一个新包
2、Java面向对象 - 图82、Java面向对象 - 图9
idea会自动帮我们分开,很人性化.
接下来. 我们到dao里写一个Person类.

  1. package com.xyq.dao;
  2. public class Person {
  3. String name;
  4. String address;
  5. public Person(String name, String address){
  6. this.name = name;
  7. this.address = address;
  8. }
  9. public void chi(){
  10. System.out.println(this.name + "正在吃东西");
  11. }
  12. }

我们到bao里调用这个类

  1. package com.xyq.bao;
  2. public class TestPerson {
  3. public static void main(String[] args) {
  4. Person p1 = new Person("武大郎", "阳谷县"); // 这行报错
  5. }
  6. }

我们发现 程序报错. 原因是. 自己包里没有这个叫Person的东西. 就好比, 你在你自己的房间里喊楼下的人. 听不见. 所以呢. 你需要打电话把楼下的人叫上来. 然后你俩面对面了. 你说什么他都能听到了. 此时, 我们需要导包,
语法:

import 包.类

  1. package com.xyq.bao; //第一行
  2. import com.xyq.dao.Person; // 导包
  3. public class TestPerson {
  4. public static void main(String[] args) {
  5. Person p1 = new Person("武大郎", "阳谷县");
  6. p1.chi();
  7. }
  8. }

在idea中导包非常简单,只需要将鼠标放到需要导入的类上 按快捷键 Alt+Enter,即可自动导包
聪明的你一定想起来了,Scanner不就这样么,对于Scanner System这类包属于java.lang

不需要导包:

  1. 在自己包里
  2. java.lang包. 我们用的String Scanner System.out.println()就是这个包里的.

image.png

六、修饰符

Java中的修饰符分为3类:

  • 权限修饰符:public、default默认、protected、private
  • 状态修饰符:static、final
  • 抽象修饰符:abstract

    1)访问权限修饰符

    这个很好理解, 你的东西你肯定不希望别人随意的看随意的访问对吧, java程序也是这样,不是啥都是对外的。有些东西自己享用就好了,有些东西是留给自己后代的,还有些东西是自己这一片邻居可以访问的,最后还有一些是大家都能访问的。
    java一共四种访问权限,参考:菜鸟教程
权限分类 当前类 子孙类 同一包(子类或无关类) 不同包(子类) 不同包(无关类)
public
protected ×
default × ×
private × × × ×
  1. package com.xyq.bao;
  2. public class Person {
  3. String def = "def"; // 默认啥都不写就是default权限
  4. public String pub = "pub"; // 公共的
  5. private String pri = "pri"; // 自己的
  6. public static void main(String[] args) {
  7. Person p = new Person();
  8. // 自己类里,都没问题
  9. System.out.println(p.def);
  10. System.out.println(p.pub);
  11. System.out.println(p.pri);
  12. }
  13. }

自己包里的其他类里试试:

  1. package com.xyq.bao;
  2. public class TestPackagePerson {
  3. public static void main(String[] args) {
  4. Person p = new Person();
  5. // 自己包里private不行了
  6. System.out.println(p.def);
  7. System.out.println(p.pub);
  8. // System.out.println(p.pri); // 报错了
  9. }
  10. }

换个包试试

  1. package com.xyq.baowai;
  2. import com.xyq.bao.Person;
  3. public class TestPackagePerson {
  4. public static void main(String[] args) {
  5. Person p = new Person();
  6. // 包外面的其他类. 只有public可以
  7. System.out.println(p.pub);
  8. // System.out.println(p.def); // 报错了
  9. // System.out.println(p.pri); // 报错了
  10. }
  11. }

一般情况, 我们很少用包访问权限. 这种权限并不舒服. 说白了. 你家里的东西要么是都能让人看的, 要么就是自己用的. 很少会专门准备一些东西给你的邻居用的. 程序也一样. 很少会用默认的访问权限.
image.png

2)状态修饰符

final修饰的特点:

  • 修饰方法:表明该方法是最终方法,不能被重写
  • 修饰变量:表明该变量是常量,不能再次被赋值
  • 修饰类:表明该类是最终类,不能被继承

final修饰局部变量

  • 变量是基本类型:final修饰指的是基本类型的数据值不能改变
  • 变量是引用类型:final修饰指的是引用类型的地址值不能改变,但是地址里面的内容可以改变

static关键字是静态的意思,可以修饰成员变量、成员方法
static修饰特点:

  • 被类的所有对象共享
  • 可以通过类名调用(也可以通过对象名调用,推荐使用类名调用)
  • static修饰的静态方法只能访问静态成员和静态方法
  • 非静态方法可以访问静态成员和静态方法也可以访问非静态成员和非静态方法

    七、getter和setter

    GetterSetter.java 定义了私有属性name age

    1. package com.xiaomi.entity;
    2. public class GetterSetter {
    3. private String name;
    4. private int age;
    5. //获取name
    6. public String getName() {
    7. return name;
    8. }
    9. //设置name
    10. public void setName(String name) {
    11. if(name.length() >1){
    12. this.name = name;
    13. }else{
    14. this.name = "匿名";
    15. }
    16. }
    17. //获取age
    18. public int getAge() {
    19. return age;
    20. }
    21. //设置age
    22. public void setAge(int age) {
    23. if(age<0){
    24. this.age = 0;
    25. }else{
    26. this.age = age;
    27. }
    28. }
    29. public void chi(){
    30. System.out.println(this.name+"在吃东西");
    31. }
    32. }

    包下面的其他类TestGS访问和设置GetterSetter.java中的私有属性

    1. package com.xiaomi.entity;
    2. public class TestGS {
    3. public static void main(String[] args) {
    4. GetterSetter obj1 = new GetterSetter();
    5. // obj1.name = "周杰伦"; //name是私有
    6. obj1.setName("周杰伦");
    7. obj1.setAge(-1);
    8. obj1.chi();
    9. System.out.println(obj1.getAge());
    10. }
    11. }

    上面例子中我们把成员变量用private保护起来,然后给出set和get方法, 在外界访问这个属性的时候,就需要使用set和get方法了. 那这里的get和set就是getter和setter方法.
    上面发现对私有属性的保护要重写get set方法很浪费时间,IDEA帮我们提供了快捷方法
    快捷键: 空白处, 右键-> generate -> getter and setter
    一键生成get set 方法

    八、继承

    1.继承小结

  • 继承是面向对象编程的一种强大的代码复用方式;

  • Java只允许单继承,所有类最终的根类是Object
  • protected允许子类访问父类的字段和方法;
  • 子类的构造方法可以通过super()调用父类的构造方法;
  • 可以安全地向上转型为更抽象的类型;
  • 可以强制向下转型,最好借助instanceof判断;
  • 子类和父类的关系是is,has关系不能用继承。

    2.基本用法

    继承: 子类可以自动拥有父类中除了私有内容外的其他所有内容。
    语法:

    public class 子类 extends 父类{ }

那什么样的逻辑我们可以写成继承关系呢? 当出现xxx是一种xxxx的时候,就可以用继承关系。比如,学生是人,
黑熊精是妖怪,猫 狗 是动物。
1.首先定义了Person类:

  1. class Person {
  2. private String name;
  3. private int age;
  4. public String getName() {...}
  5. public void setName(String name) {...}
  6. public int getAge() {...}
  7. public void setAge(int age) {...}
  8. }

2.现在假设需要定义一个Student类,字段如下:

  1. class Student {
  2. private String name;
  3. private int age;
  4. private int score;
  5. public String getName() {...}
  6. public void setName(String name) {...}
  7. public int getAge() {...}
  8. public void setAge(int age) {...}
  9. public int getScore() { }
  10. public void setScore(int score) { }
  11. }

仔细观察,发现Student类包含了Person类已有的字段和方法,只是多出了一个score字段和相应的getScore()setScore()方法。
能不能在Student中不要写重复的代码?
这个时候,继承就派上用场了。
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让StudentPerson继承时,Student就获得了Person的所有功能,我们只需要为Student编写新增的功能。
Java使用extends关键字来实现继承:

  1. class Person {
  2. private String name;
  3. private int age;
  4. public String getName() {...}
  5. public void setName(String name) {...}
  6. public int getAge() {...}
  7. public void setAge(int age) {...}
  8. }
  9. class Student extends Person {
  10. // 不要重复name和age字段/方法,
  11. // 只需要定义新增score字段/方法:
  12. private int score;
  13. public int getScore() { }
  14. public void setScore(int score) { }
  15. }

可见,通过继承,Student只需要编写额外的功能,不再需要重复代码。
注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!(否则就是子类重写父类字段/方法)

在OOP的术语中,我们把Person称为超类(super class)/父类(parent class)/基类(base class),把Student称为子类(subclass)/扩展类(extended class)。叫的最多的还是 父类 子类

继承树

注意到我们在定义Person的时候,没有写extends。在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。下图是PersonStudent的继承树:
2、Java面向对象 - 图12
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
类似的,如果我们定义一个继承自PersonTeacher,它们的继承树关系如下:
2、Java面向对象 - 图13

protected

希望父类的某个属性/方法能被子类重写,但不希望被其他类自由访问,可以使用protected来修饰。
继承有个特点,就是子类无法访问父类的private字段或者private方法。例如,Student类就无法访问Person类的nameage字段:

  1. class Person {
  2. private String name;
  3. private int age;
  4. }
  5. class Student extends Person {
  6. public String hello() {
  7. return "Hello, " + name; // 编译错误:无法访问name字段
  8. }
  9. }

这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问:

  1. class Person {
  2. protected String name;
  3. protected int age;
  4. }
  5. class Student extends Person {
  6. public String hello() {
  7. return "Hello, " + name; // OK!
  8. }
  9. }

因此,protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问,后面我们还会详细讲解。

super

super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName。例如:

  1. class Person {
  2. protected String name;
  3. protected int age;
  4. }
  5. class Student extends Person {
  6. public String hello() {
  7. return "Hello, " + super.name; //name this.name super.name 效果一样
  8. }
  9. }

实际上,这里使用super.name,或者this.name,或者name,效果都是一样的。编译器寻找路径是:自己类 —> 父类 ,自己类中没有就去父类中找。
但是,在某些时候,就必须使用super。我们来看一个例子:

  1. public class Main {
  2. public static void main(String[] args) {
  3. Student s = new Student("Xiao Ming", 12, 89);
  4. }
  5. }
  6. class Person {
  7. protected String name;
  8. protected int age;
  9. public Person(String name, int age) {
  10. this.name = name;
  11. this.age = age;
  12. }
  13. }
  14. class Student extends Person {
  15. protected int score;
  16. public Student(String name, int age, int score) {
  17. this.score = score;
  18. }
  19. }

运行上面的代码,会得到一个编译错误,大意是在Student的构造方法中,无法调用Person的构造方法。
这是因为在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会自动帮我们自动加一句super();,所以Student类的构造方法实际上是这样:

  1. class Student extends Person {
  2. protected int score;
  3. public Student(String name, int age, int score) {
  4. super(); // 但是父类中的构造方法是带参数,因此这里报错
  5. this.score = score;
  6. }
  7. }

但是,当前Person类并没有无参数的构造方法,因此,编译失败。
解决方法是调用Person类存在的某个构造方法。例如:

  1. class Student extends Person {
  2. protected int score;
  3. public Student(String name, int age, int score) {
  4. super(name, age); // 调用父类的构造方法Person(String, int)
  5. this.score = score;
  6. }
  7. }

这样就可以正常编译了!
因此我们得出结论:如果父类没有默认的构造方法(即我们重写了父类的构造方法),子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。
这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
image.png
子类的构造方法中第一句默认是super()调用父类的无参构造方法,如果想要调用父类的有参构造方法需要手动写super("xiong",26),所以我们写类时一般都会定义一个无参构造方法。

3.向上转型

如果一个引用变量的类型是Student,那么它可以指向一个Student类型的实例:

  1. Student s = new Student();

如果一个引用类型的变量是Person,那么它可以指向一个Person类型的实例:

  1. Person p = new Person();

现在问题来了:如果Student是从Person继承下来的,那么,一个引用类型为Person的变量,能否指向Student类型的实例?

  1. Person p = new Student(); // ???

测试一下就可以发现,这种指向是允许的!
这是因为Student继承自Person,因此,它拥有Person的全部功能。Person类型的变量,如果指向Student类型的实例,对它进行操作,是没有问题的!
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:

  1. Student s = new Student();
  2. Person p = s; // upcasting, ok
  3. Object o1 = p; // upcasting, ok
  4. Object o2 = s; // upcasting, ok

注意到继承树是Student > Person > Object,所以,可以把Student类型转型为Person,或者更高层次的Object

4.向下转型

和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:

  1. Person p1 = new Student(); // upcasting, ok
  2. Person p2 = new Person();
  3. Student s1 = (Student) p1; // ok
  4. Person s2 = (Person) p2; // ok,自己对自己强转
  5. Student s2 = (Student) p2; // runtime error! ClassCastException!父类不能转到子类

如果测试上面的代码,可以发现:
Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException

instanceof操作符

为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:

  1. Person p = new Person();
  2. System.out.println(p instanceof Person); // true
  3. System.out.println(p instanceof Student); // false
  4. Student s = new Student();
  5. System.out.println(s instanceof Person); // true
  6. System.out.println(s instanceof Student); // true
  7. Student n = null;
  8. System.out.println(n instanceof Student); // false

instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false
利用instanceof,在向下转型前可以先判断:

  1. Person p = new Student();
  2. if (p instanceof Student) {
  3. // 只有判断成功才会向下转型:
  4. Student s = (Student) p; // 一定会成功
  5. }

从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:

  1. Object obj = "hello";
  2. if (obj instanceof String) {
  3. String s = (String) obj;
  4. System.out.println(s.toUpperCase());
  5. }

可以改写如下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. Object obj = "hello";
  4. if (obj instanceof String s) {
  5. // 可以直接使用变量s:
  6. System.out.println(s.toUpperCase());
  7. }
  8. }
  9. }

使用instanceof variable这种判断并转型为指定类型变量的语法时,必须打开编译器开关--source 14--enable-preview

image.png

5.区分继承和组合

考察下面的Book类:

  1. class Book {
  2. protected String name;
  3. public String getName() {...}
  4. public void setName(String name) {...}
  5. }

这个Book类也有name字段,那么,我们能不能让Student继承自Book呢?

  1. class Student extends Book {
  2. protected int score;
  3. }

显然,从逻辑上讲,这是不合理的,Student不应该从Book继承,而应该从Person继承。
究其原因,是因为StudentPerson的一种,它们是is关系,而Student并不是Book。实际上StudentBook的关系是has关系。
具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例:

  1. class Student extends Person {
  2. protected Book book;
  3. protected int score;
  4. }

因此,继承是is关系,组合是has关系。
参考:https://blog.csdn.net/weixin_43819113/article/details/90273844
练习:
2、Java面向对象 - 图16
Main.java

  1. public class Main {
  2. public static void main(String[] args) {
  3. Person p = new Person("小明", 12);
  4. //此时需要显示的创建被组合的对象 b1
  5. Book b1 = new Book("三体",89.2);
  6. Student s = new Student("小红", 20, 99,b1);
  7. s.BookContent();
  8. // TODO: 定义PrimaryStudent,从Student继承,新增grade字段:
  9. Student ps = new PrimaryStudent("小军", 9, 87,b1,5);
  10. System.out.println(ps.getScore());
  11. ps.BookContent();
  12. }
  13. }

Person.java

  1. public class Person {
  2. protected String name;
  3. protected int age;
  4. public Person(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. public String getName() {
  9. return name;
  10. }
  11. public void setName(String name) {
  12. this.name = name;
  13. }
  14. public int getAge() {
  15. return age;
  16. }
  17. public void setAge(int age) {
  18. this.age = age;
  19. }
  20. }

Student.java

  1. public class Student extends Person {
  2. protected int score;
  3. protected Book book;
  4. public Student(String name, int age, int score,Book book) {
  5. super(name, age);
  6. this.book = book;
  7. this.score = score;
  8. }
  9. public int getScore() {
  10. return score;
  11. }
  12. //重写该方法
  13. public void BookContent(){
  14. System.out.println("我是Student类,"+this.name+"有这本书!");
  15. this.book.BookContent();
  16. }
  17. public void BookSale(){
  18. System.out.println("这本书销量非常好,已经供不应求了!");
  19. }
  20. }

PrimaryStudent.java

  1. public class PrimaryStudent extends Student {
  2. protected int grade;
  3. public PrimaryStudent(String name, int age, int score,Book b1,int grade) {
  4. super(name,age,score,b1);
  5. this.grade = grade;
  6. }
  7. }

Book.java

  1. public class Book {
  2. protected String bookname;
  3. protected Double price;
  4. protected String author;
  5. protected String type;
  6. public Book(String bookname,Double price){
  7. this.bookname = bookname;
  8. this.price = price;
  9. }
  10. public void BookContent(){
  11. System.out.println("我是Book类,这本书的书名是:"+this.bookname+",价格是:"+this.price);
  12. }
  13. }

九、Java多态

Java多态特点

多态—-顾名思义就是一种事物表现出不同的形态;举个例子:汤姆猫既是猫类(拥有猫捉老鼠的特性) 也是动物类(动物会跑会吃东西的特性)。

多态中成员访问特点

  • 成员变量:编译看左边,执行看左边
  • 成员方法:编译看左边,执行看右边

这两句话怎么理解呢?

Animal a1 = new Dog(); a1.name 成员变量:编译时Animal类中必须要有name属性,执行时也是运行的Animal中的name

Animal a1 = new Dog(); a1.eat(); 成员方法:编译时Animal类中必须要有eat方法(可以是抽象方法),执行时运行的是右边Dog中的eat方法

为什么成员变量和成员方法的访问不一样呢?

因为成员方法有重写,而成员变量没有

image.png

重写(Override)与重载(Overload)

重写(Override)

重写是子类对父类的允许访问的方法的实现进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。 重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。 在面向对象原则里,重写意味着可以重写任何现有方法。实例如下: ```java class Animal{ public void move(){ System.out.println(“动物可以移动”); } }

class Dog extends Animal{ public void move(){ System.out.println(“狗可以跑和走”); } }

public class TestDog{ public static void main(String args[]){ Animal a = new Animal(); // Animal 对象 Animal b = new Dog(); // Dog 对象

  1. a.move();// 执行 Animal 类的方法
  2. b.move();//执行 Dog 类的方法

} }

  1. 以上实例编译运行结果如下:

动物可以移动 狗可以跑和走

  1. 在上面的例子中可以看到,尽管 b 属于 Animal 类型,但是它运行的是 Dog 类的 move方法。<br />这是由于在编译阶段,只是检查参数的引用类型。<br />然而在运行时,Java 虚拟机(JVM)指定对象的类型并且运行该对象的方法。<br />因此在上面的例子中,之所以能编译成功,是因为 Animal 类中存在 move 方法,然而运行时,运行的是特定对象的方法。<br />思考以下例子:
  2. ```java
  3. package com.polymorphism.entity;
  4. public class OverrideOverload {
  5. public static void main(String args[]){
  6. Animal a = new Animal(); // Animal 对象
  7. Animal b = new Dog(); // Dog 对象
  8. a.move(); // 执行 Animal 类的方法
  9. b.move(); //执行 Dog 类的方法
  10. // b.bark(); //编译失败,是因为 Animal 类中不存在 bark 方法
  11. Dog d1 = new Dog();
  12. d1.bark(); //编译成功,是因为 Dog 类中存在 bark 方法
  13. }
  14. }
  15. class Animal{
  16. public void move(){
  17. System.out.println("动物可以移动");
  18. }
  19. }
  20. class Dog extends Animal{
  21. public void move(){
  22. System.out.println("狗可以跑和走");
  23. }
  24. public void bark(){
  25. System.out.println("狗可以吠叫");
  26. }
  27. }

方法的重写规则

  • 参数列表必须完全与被重写方法的相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。(public>默认>protected>private)
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 被访问控制符private修饰的方法不能被重写。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个方法,则不能重写这个方法。

    重载(Overload)

    重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
    每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
    最常用的地方就是构造器的重载。
    重载规则:

  • 被重载的方法必须改变参数列表(参数个数、参数顺序、参数类型 不同都能造成重载);

  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

实例:

  1. public class Overloading {
  2. public int test(){
  3. System.out.println("test1");
  4. return 1;
  5. }
  6. public void test(int a){
  7. System.out.println("test2");
  8. }
  9. //以下两个参数类型顺序不同
  10. public String test(int a,String s){
  11. System.out.println("test3");
  12. return "returntest3";
  13. }
  14. public String test(String s,int a){
  15. System.out.println("test4");
  16. return "returntest4";
  17. }
  18. public static void main(String[] args){
  19. Overloading o = new Overloading();
  20. System.out.println(o.test());
  21. o.test(1);
  22. System.out.println(o.test(1,"test3"));
  23. System.out.println(o.test("test4",1));
  24. }
  25. }

打印:

test1 1 test2 test3 returntest3 test4 returntest4

重写与重载之间的区别

区别点 重载方法overloading 重写方法overriding
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问 可以修改 一定不能做更严格的限制(可以降低限制)

总结:

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • 方法重载是一个类中定义了多个方法名相同,而他们的参数不同,则称为方法的重载(Overloading)。
  • 方法重写是在子类中存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

2、Java面向对象 - 图18
2、Java面向对象 - 图19
_@_Override
是伪代码,表示重写。(当然不写@Override也可以),不过写上有如下好处:
1、可以当注释用,方便阅读;
2、编译器可以给你验证@Override下面的方法是否正确的重写父类方法,如果没有则报错。例如,你如果没写@Override,而你下面的方法名又写错了,这时你的编译器是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法。

多态特性

https://www.liaoxuefeng.com/wiki/1252599548343744/1260455778791232
小结:

  • 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为;
  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;
  • final修饰符有多种作用:

    • final修饰的方法可以阻止被覆写;
    • final修饰的class可以阻止被继承;
    • final修饰的字段必须在创建对象时初始化,随后不可修改。

      覆写(Override)

      在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
      例如,在Person类中,我们定义了run()方法:
      1. class Person {
      2. public void run() {
      3. System.out.println("Person.run");
      4. }
      5. }
      在子类Student中,覆写这个run()方法:
      1. class Student extends Person {
      2. @Override
      3. public void run() {
      4. System.out.println("Student.run");
      5. }
      6. }
      Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override
      注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
      1. class Person {
      2. public void run() { }
      3. }
      4. class Student extends Person {
      5. // 不是Override,因为参数不同:
      6. public void run(String s) { }
      7. // 不是Override,因为返回值不同:
      8. public int run() { }
      9. }
      加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
      1. public class Main {
      2. public static void main(String[] args) {
      3. }
      4. }
      5. class Person {
      6. public void run() {}
      7. }
      8. public class Student extends Person {
      9. @Override // Compile error!
      10. public void run(String s) {}
      11. }
      但是@Override不是必需的。
      在上一节中,我们已经知道,引用变量的声明类型可能与其实际类型不符,例如:
      1. Person p = new Student();
      现在,我们考虑一种情况,如果子类覆写了父类的方法:
      1. public class Main {
      2. public static void main(String[] args) {
      3. Person p = new Student();
      4. p.run(); // 应该打印Person.run还是Student.run?
      5. }
      6. }
      7. class Person {
      8. public void run() {
      9. System.out.println("Person.run");
      10. }
      11. }
      12. class Student extends Person {
      13. @Override
      14. public void run() {
      15. System.out.println("Student.run");
      16. }
      17. }
      那么,一个实际类型为Student,引用类型为Person的变量,调用其run()方法,调用的是Person还是Studentrun()方法?
      运行一下上面的代码就可以知道,实际上调用的方法是Studentrun()方法。因此可得出结论:
      Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
      这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。

      多态

      多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。例如:
      1. Person p = new Student();
      2. p.run(); // 无法确定运行时究竟调用哪个run()方法
      有童鞋会问,从上面的代码一看就明白,肯定调用的是Studentrun()方法啊。
      但是,假设我们编写这样一个方法:
      1. public void runTwice(Person p) {
      2. p.run();
      3. p.run();
      4. }
      它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类,因此,也无法确定调用的是不是Person类定义的run()方法。
      所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。这种不确定性的方法调用,究竟有什么作用?
      我们还是来举栗子。
      假设我们定义一种收入,需要给它报税,那么先定义一个Income类:
      1. class Income {
      2. protected double income;
      3. public double getTax() {
      4. return income * 0.1; // 税率10%
      5. }
      6. }
      对于工资收入,可以减去一个基数,那么我们可以从Income派生出SalaryIncome,并覆写getTax()
      1. class Salary extends Income {
      2. @Override
      3. public double getTax() {
      4. if (income <= 5000) {
      5. return 0;
      6. }
      7. return (income - 5000) * 0.2;
      8. }
      9. }
      如果你享受国务院特殊津贴,那么按照规定,可以全部免税:
      1. class StateCouncilSpecialAllowance extends Income {
      2. @Override
      3. public double getTax() {
      4. return 0;
      5. }
      6. }
      现在,我们要编写一个报税的财务软件,对于一个人的所有收入进行报税,可以这么写:
      1. public double totalTax(Income... incomes) {
      2. double total = 0;
      3. for (Income income: incomes) {
      4. total = total + income.getTax();
      5. }
      6. return total;
      7. }
      来试一下:
      1. public class Main {
      2. public static void main(String[] args) {
      3. // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
      4. Income[] incomes = new Income[] {
      5. new Income(3000),
      6. new Salary(7500),
      7. new StateCouncilSpecialAllowance(15000)
      8. };
      9. System.out.println(totalTax(incomes)); //800
      10. }
      11. public static double totalTax(Income... incomes) {
      12. double total = 0;
      13. for (Income income: incomes) {
      14. total = total + income.getTax();
      15. }
      16. return total;
      17. }
      18. }
      19. class Income {
      20. protected double income;
      21. public Income(double income) {
      22. this.income = income;
      23. }
      24. public double getTax() {
      25. return income * 0.1; // 税率10%
      26. }
      27. }
      28. class Salary extends Income {
      29. public Salary(double income) {
      30. super(income);
      31. }
      32. @Override
      33. public double getTax() {
      34. if (income <= 5000) {
      35. return 0;
      36. }
      37. return (income - 5000) * 0.2;
      38. }
      39. }
      40. class StateCouncilSpecialAllowance extends Income {
      41. public StateCouncilSpecialAllowance(double income) {
      42. super(income);
      43. }
      44. @Override
      45. public double getTax() {
      46. return 0;
      47. }
      48. }
      观察totalTax()方法:利用多态,totalTax()方法只需要和Income打交道,它完全不需要知道SalaryStateCouncilSpecialAllowance的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。
      可见,多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

      覆写Object方法

      因为所有的class最终都继承自Object,而Object定义了几个重要的方法:
  • toString():把instance输出为String

  • equals():判断两个instance是否逻辑相等;
  • hashCode():计算一个instance的哈希值。

在必要的情况下,我们可以覆写Object的这几个方法。例如:

  1. class Person {
  2. ...
  3. // 显示更有意义的字符串:
  4. @Override
  5. public String toString() {
  6. return "Person:name=" + name;
  7. }
  8. // 比较是否相等:
  9. @Override
  10. public boolean equals(Object o) {
  11. // 当且仅当o为Person类型:
  12. if (o instanceof Person) {
  13. Person p = (Person) o;
  14. // 并且name字段相同时,返回true:
  15. return this.name.equals(p.name) && (this.age == p.age);
  16. }
  17. return false;
  18. }
  19. // 计算hash:
  20. @Override
  21. public int hashCode() {
  22. return this.name.hashCode();
  23. }
  24. }

调用super-重写父类

在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。例如:

  1. class Person {
  2. protected String name;
  3. public String hello() {
  4. return "Hello, " + name;
  5. }
  6. }
  7. Student extends Person {
  8. @Override
  9. public String hello() {
  10. // 调用父类的hello()方法:
  11. return super.hello() + "!";
  12. }
  13. }

final不可变

继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override

  1. class Person {
  2. protected String name;
  3. public final String hello() {
  4. return "Hello, " + name;
  5. }
  6. }
  7. Student extends Person {
  8. // compile error: 不允许覆写
  9. @Override
  10. public String hello() {
  11. }
  12. }

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承:

  1. final class Person {
  2. protected String name;
  3. }
  4. // compile error: 不允许继承自Person
  5. Student extends Person {
  6. }

对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改。例如:

  1. class Person {
  2. public final String name = "Unamed";
  3. }

final字段重新赋值会报错:

  1. Person p = new Person();
  2. p.name = "New Name"; // compile error!

可以在构造方法中初始化final字段:

  1. class Person {
  2. public final String name;
  3. public Person(String name) {
  4. this.name = name;
  5. }
  6. }

这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。

练习:

2、Java面向对象 - 图20
参考:https://book.apeland.cn/details/126/