[Java 学习笔记] 二.面向对象基础

  

目录

对象与实例

可变参数

构造方法

既对字段进行初始化,又在构造方法中对字段进行初始化

方法重载

继承

补充:阻止继承

向下转型时的注意事项

 多态

final用处

抽象类

静态字段

 作用域

经验

接口

接口与类

抽象类和接口的区别

今日感受 



笔记总结自:

Java教程 - 廖雪峰的官方网站 (liaoxuefeng.com)https://www.liaoxuefeng.com/wiki/1252599548343744​​前文回顾​​​​​: [Java 学习笔记] 一.部分基础细节_☆迷茫狗子的秘密基地☆-CSDN博客

 


对象与实例

定义了class,只是定义了对象模版,而要根据对象模版创建出真正的对象实例(instance),必须用new操作符。

new操作符可以创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:

Person ming = new Person();

上述代码创建了一个Person类型的实例,并通过变量ming指向它。

注意区分Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。

有了指向这个实例的变量,我们就可以通过这个变量来操作实例。

若干实例拥有class定义的字段(field),且各自都有一份独立的数据,互不干扰。

 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中。


可变参数

可变参数用类型...定义,可变参数相当于数组类型:

class Group {
    private String[] names;

    public void setNames(String... names) {
        this.names = names;
    }
}

上面的setNames()就定义了一个可变参数。调用时,可以这么写:

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String

完全可以把可变参数改写为String[]类型:

class Group {
    private String[] names;

    public void setNames(String[] names) {
        this.names = names;
    }
}

但是,调用方需要自己先构造String[],比较麻烦。例如:

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]

另一个问题是,调用方可以传入null

Group g = new Group();
g.setNames(null);

而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null


构造方法

没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false 

也可以对字段直接进行初始化:

class Person {
    private String name = "Unamed";
    private int age = 10;
}

那么问题来了:

既对字段进行初始化,又在构造方法中对字段进行初始化

class Person {
    private String name = "Unamed";
    private int age = 10;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

当我们创建对象的时候,new Person("Xiao Ming", 12)得到的对象实例,字段的初始值是啥?

在Java中,创建对象实例的时候,按照如下顺序进行初始化:

  1. 先初始化字段,例如,int age = 10;表示字段初始化为10double salary;表示字段默认初始化为0String name;表示引用类型字段默认初始化为null

  2. 执行构造方法的代码进行初始化。

因此,构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)的字段值最终由构造方法的代码确定

一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this(name, 18); // 调用另一个构造方法Person(String, int)
    }

    public Person() {
        this("Unnamed"); // 调用另一个构造方法Person(String)
    }
}

方法重载

方法名相同,但各自的参数不同,称为方法重载(Overload)。

注意:方法重载的返回值类型通常都是相同的


继承

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

  • Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
  • 继承有个特点,就是子类无法访问父类的private字段或者private方法。这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问,可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super(), 另外,如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法

这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

补充:阻止继承

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。

从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。

例如,定义一个Shape类:

public sealed class Shape permits Rect, Circle, Triangle {
    ...
}

上述Shape类就是一个sealed类,它只允许指定的3个类继承它

这种sealed类主要用于一些框架,防止继承被滥用。

sealed类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview--source 15


把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)

向下转型时的注意事项

向下转型:把一个父类类型强制转型为子类类型

Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

如果测试上面的代码,可以发现:

Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来

因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException

为了避免向下转型出错,Java提供了instanceof操作符,可以判断一个变量所指向的实例是否是指定类型,或者这个类型的子类

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

利用instanceof,在向下转型前可以先判断:

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

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

Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}

可以改写如下:

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

 多态

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。

下面一个简单的"人"类,具有若干收入,现在要获取总税收

import java.lang.*;

class Person{
    String name;
    private Income[] incomes;

    Person(String name,double in,double sa, double sp){
        this.name = name;
        incomes = new Income[]{
            new Income(in),
            new Salary(sa),
            new StateCouncilSpecialAllowance(sp)
        };
    }

    public double getTotaTax(){
        double total = 0;
        for (Income part : incomes){
            total += part.getTax();
        }
        return total;
    }
}
//普通收入
class Income{
    protected double income;

    public Income(double income){
        this.income = income;
    }

    public double getTax(){
        return income*0.1; //税率 10%
    }
}
//工资收入
class Salary extends Income{
    public Salary(double income){
        super(income);
    }
    @Override
    public double getTax(){
        if(income <= 5000){
            return 0;
        }else{
            return (income - 5000)*0.2;
        }
    }
}
//国务院特殊津贴
class StateCouncilSpecialAllowance  extends Income{
    public StateCouncilSpecialAllowance(double income){
        super(income);
    }
    @Override
    public double getTax(){
        return 0;
    }
}
public class demo {
    public static void main(String[] args) {
        Person LiMing = new Person("李明",4000,5500,12000);
        System.out.println(LiMing.getTotaTax());
    }
}

如果我们要新增一种收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入getTotalTax(),不需要修改任何代码,

由此可见多态能允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码

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

class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    @Override
    public String hello() {
        // 调用父类的hello()方法:
        return super.hello() + "!";
    }
}

final用处

  • 阻止被覆写:不允许子类对父类的某个方法进行覆写
  • 阻止被继承:一个类不希望任何其他类继承自它
  • 阻止再次修改:字段在初始化后不能被修改


抽象类

抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。

尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person);

  • 不需要子类就可以实现业务逻辑(正常编译);

  • 具体的业务逻辑由不同的子类实现,调用者并不关心。


静态字段

不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象


注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。

Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:

  • 如果是完整类名,就直接根据完整类名查找这个class

  • 如果是简单类名,按下面的顺序依次查找:

    • 查找当前package是否存在这个class

    • 查找import的包是否包含这个class

    • 查找java.lang包是否包含这个class

如果按照上面的规则还无法确定类名,则编译报错。

注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入 


 作用域

定义为publicfieldmethod可以被其他类访问,前提是首先有访问class的权限

推荐private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法

Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:

public class Main {
    public static void main(String[] args) {
        Inner i = new Inner();
        i.hi();
    }

    // private方法:
    private static void hello() {
        System.out.println("private hello!");
    }

    // 静态内部类:
    static class Inner {
        public void hi() {
            Main.hello();
        }
    }
}

经验

如果不确定是否需要public,就不声明为public,即尽可能少地暴露对外的字段和方法。

把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。


接口

在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface

interface: 就是比抽象类还要抽象的纯抽象接口,连字段都不能有。因为接口定义的所有方法默认都是也只能是public abstract, 所以这两个修饰符不需要写出来

接口与类

相似点区别
  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。
  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

抽象类和接口的区别

  • 1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。


今日感受 

——今日毕,果然系统地过一遍发现了很多学的时候遗漏或是淡泊的点,学校的进度暂且到这里,

接下来的自学速度会放慢,

另外接口这一块明天很有必要斟酌一番 . . .

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泥烟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值