Java基础 - 面向对象


在Java中有句俗语叫作 “万物皆对象”,这句话的意思是我们可以把Java程序看成是很多个对象构建的系统。具体来说就是将Java程序的所有参与角色都看成一个个对象,通过对象与对象之间的相互调用来完成系统的功能。这是一种将程序模块化的思想,叫作面向对象编程思想。

什么是面向对象

在面向对象编程思想之前,程序开发采用的是面向过程的编程方式,它的思想是自上而下,将一个大问题分解成几个小问题,在将小问题分解更小的问题,然后将任务划分为一个个步骤,按照步骤去执行。所有这种方式的开发步骤是非常繁琐的,同时制约了程序的可维护性和可扩展性

通过一个例子来说明:如果现在需要开车去北京,按照面向过程的思想,需要处理的步骤是,第一步,打开车门,第二步,坐进驾驶室,第三步,发动汽车,第四步,踩油门 …… 第n步,到达停车。整个过程需要亲历亲为非常麻烦。如果使用面向对象来处理,就是将程序的参与者全部模块化对象,现在就可以模块化三个对象:人,汽车,北京。然后只需要让这3个对象相互调用就可以完成需求。面向过程注重的是每一个步骤,面向对象关注的是整件事情的模块化结构。

面向对象的核心思想是重用性和灵活性。重用性是指已经写好的某个业务代码,可以在多个不同的功能模块中复用。而不必针对每个功能模块都去编写相同的代码。灵活性是指当某个功能模块完成之后,根据需求的变化可以迅速在原有的代码基础上更新业务逻辑,以实现新的需求。

Java是一门面向对象的编程语言,Java程序的功能都是通过对象来完成的,根据业务需求将程序分解为不同的对象,再通过对象之间的相互调用关系来协同实现相应的功能。

类与对象

在Java的世界中,我们可以把世间万物都看作对象,例如一个人、一间房子、一台电脑等,总之任何事物我们都可以将其看作对象,然后通过对象之间的相互调用来完成需求。通过属性和方法区分对象中的差异。属性指的是对象的静态特征,例如一个人的年龄、身高等。方法用来描述对象的动态特征,例如一个人可以跑步、说话等。总结:对象就是用来描述客观存在的一个实体,该实体由一组属性和方法构成。

类与对象的关系

类是用来组织Java程序的,一个Java程序就是一个类。

类是产生对象的模板,所有的对象都是通过类来创建的;类是对象的抽象化描述,对象是类的具体实例。

类是抽象概念,是一种描述,仅仅是模板,对象是实实在在的具体存在的,对象和类是面向对象编程思想的核心。

定义类

public class 类名{
    // 定义属性,声明数据的公私有属性  数据类型  属性名
    public int name;
    // 定义方法,声明方法的公私有属性  返回值类型 方法名(参数列表){}
    public void getName(){
        // 方法体
    }
}

定义属性需要指定其数据类型;定义方法需要确定返回值类型和参数列表
定义方法中的返回值类型有两种:有返回值和无返回值。有返回值的方法需要在方法定义时指定返回值数据类型,并在方法体中用return将结果返回给外部调用者。如果一个方法没有返回值,其返回数据类型是void。

// 有返回值
public Sting getName(){
    return "小红";
}
// 无返回值
public void getName(){
    System.out.println("小红");
}

参数列表是指外部在调用该方法时需要传到方法内部进行运算的数据。

public String getName(String name){
    String name1 = "小明";
    return name1 + "之后下一位是:" + name;
}

构造函数

Java是通过类的构造函数,也叫构造方法来创建对象的。构造函数是一种特殊的方法,方法名必须与类名一致,不需要定义返回值类型

构造函数包括有参构造和无参构造。每个类默认都有一个无参构造函数,在定义类的时候不需要声明无参构造函数,便可直接调用来创建对象。如果手动定义一个有参构造函数,就会覆盖掉默认的无参构造函数。

public class User{
    // 定义属性
    private int id;
    private String name;
    private int age;
    // 无参构造函数
    public User(){

    }
    // 有参构造函数
    public User(int id, String name, int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

创建对象

创建对象只需要调用对应类的构造函数即可。类中只有无参构造时,可以直接调用构造函数创建对象,不会对属性进行赋值,需要手动对属性进行赋值,先创建后赋值;

存在有参构造时,需要在调用构造函数创建对象时将属性值作为参数传入即可,边创建边赋值。

// 无参构造创建对象
User user = new User();
user.id = 1;
user.name = "aa";
user.age = 18;
// 有参构造创建对象
User user = new User(1,"aa",18);

使用对象

对象的使用包括获取和修改属性,调用方法。获取属性通过“对象名.属性名”进行获取。调用方法通过“对象名.方法名(参数列表)”来完成

User user = new User(1,"aa",18);
// 获取属性
System.out.println("用户名:" + user.name);
// 修改属性
user.age = 19;
System.out.println("用户年龄:" + user.age);
// 调用方法
user.show();

this关键字

在类的定义中通常会使用this关键字,this用来指代当前类的实例化对象,通过this可以调用当前类的属性和方法,比如在有参构造函数中,通过 this将外部传来的值赋给当前类的实例化对象。

this除了可以在类中访问属性也可以在类中调用方法。使用this调用构造函数和普通方法是不同的。调用构造函数的语法是:this(参数列表);调用普通方法的语法是:this.方法名(参数列表);。不同在普通方法中使用this来调用构造函数。

public class User(){
    public User(int id, String name, int age){
        // 调用无参构造函数
        this();
        // 调用普通方法
        this.show();
    }
}

方法重载

方法重载是java代码复用的一种重要方式,指的是两个方法之间的一种关系。方法重载的条件是:

  • 同一个类中
  • 方法名相同
  • 参数列表不同(个数或者类型不同)
  • 与返回值和访问权限修饰符无关
public class Test(){
    public static void main(String[] args){
        Test test = new Test();
    }
    public void method(){
        System.out.println("没有参数");
    }
    public void method(int id){
        System.out.println("有参数:" + id);
    }
}

成员变量和局部变量

变量是Java程序中表示数据的基本单位。变量的作用域是指在程序中可以通过变量名来访问变量的范围,变量的作用域被声明时所在的位置决定,Java中根据不同的作用域将变量分为成员变量和局部变量。

如果一个变量在方法中声明,则该变量为局部变量。如果一个变量在方法外,在类中声明,则该变量为成员变量。成员变量的作用域在整个类中,类中的每个方法都可以访问该变量。局部变量的作用域只在定义该变量的方法中,出了方法体就无法访问。

当成员变量和局部变量重名时局部变量的优先级更高
Java会给成员变量赋初始值,局部变量则不会赋初始值(因此定义局部变量不赋初始值会报错),不同类型的成员变量的初始值不同。

public class Main {
    public static String name;
    public static int num;
    public static void a(){
        System.out.println(name);
        System.out.println(num);
    }
    public static void m(){
        String na = "1";
        int n = 1;
        System.out.println(na);
        System.out.println(n);
    }
    public static void main(String[] args) {
        a();
        System.out.println("----");
        m();
    }
}

// 输出
null
0
----
1
1

面向对象的三大特征:封装、继承、多态

封装

在之前定义的类中存在一个bug,就是所有的属性都是赋予public属性,即外部在实例化类对象时,可以随意给对象的属性赋值,只要数据类型一致即可。

解决的方法是在类中对属性的赋值加以限制,将外部传来的值进行筛选,合格的完成赋值,不合格的加以处理,这个过程就是封装。

封装是指将类的属性隐藏在内部,外部不能直接访问和修改,必须通过类提供的方法来完成对属性的访问和修改。封装的核心思想就是尽可能把属性都隐藏在内部,对外提供方法来访问,我们可以在这些方法中添加逻辑处理来实现过滤,以屏蔽错误数据的赋值。

封装的步骤

(1)修改属性的访问权限,使得外部不能够直接访问
(2)提供外部可以直接调用的方法
(3)在方法中加入属性控制逻辑
属性的访问权限分为:public(公有)、private(私有)、protected(保护)

static关键字

static表示静态或全局,可以用来修饰成员变量和成员方法以及代码块。使用static修饰的成员变量和成员方法独立于该类的任何一个实例对象,访问时不依赖于该类的对象,可以理解为该类的所有实例对象共用。

static修饰的成员变量叫作静态变量也叫作类变量,static修饰的成员方法叫作静态方法,也叫作类方法。多个对象共用,内存中只有一份,没有被static修饰的成员变量叫实例变量,没有被static修饰的成员方法叫作实例方法,一个对象对应一个,内存中有多份。

静态成员变量和静态成员方法独立于任何一个实例对象,访问这些静态资源时就无须创建对象,直接通过类即可访问。

public class Main {
    public static void main(String[] args){
        System.out.println(User.name);
        User.show();
    }
}
public class User {
    public static String name = "静态变量";
    public static void show(){
        System.out.println("静态方法");
    }
}

静态方法中不能使用this关键字,不能直接访问所属类的实例变量和实例方法,可直接访问类的静态变量和静态方法。若要访问类中的实例对象和实例方法,必须先实例化类的对象,然后通过对象去访问。

public class Main {
    public static void main(String[] args){
        System.out.println(User.name);
        User.show();
        User user = new User();
        user.shows();
    }
}
public class User {
    public static String name = "静态变量";
    public static void show(){
        System.out.println("静态方法");
    }
    public void shows(){
        System.out.println("成员方法");
    }
}

static除了可以修饰成员变量和成员方法之外,还可以修饰代码块,被static修饰的代码块叫作静态代码块。静态代码块的特点是只执行一次,在什么时候执行呢?当该类被加载到内存时执行,不需要手动调用,它会自动执行。

那么类什么时候第一次被加载?现在简单阐述Java加载类的机制。

Java代码是由类构成的,但是真正运行时是通过对象和对象之间的相互调用关系来完成需求的。即程序运行时,需要在内存中创建多个对象,对象怎么创建?需要通过类来创建,类是模板,同时这个模板只需要加载一次,所以程序在运行时,首先会将程序中用到的类加载到内存中,并且只加载一次。然后通过类来创建多个对象以完成具体的业务。被加载到内存中的类叫作运行时类,静态代码块就是在加载类的时候执行的,因为类只加载一次,所以静态代码块也只执行一次。简单理解,当代码中第一次出现某个类时,就会执行静态代码块,静态代码块只能访问静态变量和静态方法,静态代码块的定义如下。

public class User{
    public static int num;
    static{
        num++;
        System.out.println("执行静态代码块");
    }
}
public class Main {
    public static void main(String[] args){
        User user = new User();
        User user1 = new User();
        User user2 = new User();
        System.out.println(user.num);
        System.out.println(user1.num);
        System.out.println(user2.num);
    }
}

// 输出
执行静态代码块
1
1
1

由上面的代码可以看出虽然创建了三个对象,但是静态代码块只执行了一次。如果多个静态块同时存在,则按先后顺序执行。

类的构造方法用于初始化类的实例,类的静态代码块用于初始化类,给类的静态变量赋值。

继承

public class Teacher{
    private int id;
    private String name;
    private int age;
    // getter、setter方法
}
public class Student{
    private int id;
    private String name;
    private int age;
    // getter、setter方法
}

从上面的两个类中可以发现,类中的属性和方法完全一样。那么是否可以将两个类中相同的部分提取出来,另外定义一个People类,然后让Teacher类和Student类拥有People类中的属性和方法。这种代码优化的方式叫做继承。即一个类继承另一个类的属性和方法,被继承的类叫父类,继承的类叫子类。

// 继承的语法
public class Student extends People{}

继承的好处是只需要定义一个父类,子类继承父类,子类就不需要定义属性和方法,可以直接拥有父类的公有属性和方法。如果子类中有特定的属性和方法,只需要在继承的基础上,在子类中定义特有的属性和方法即可。

继承是面向对象编程思想的主要特征,Java通过继承可以实现代码复用。Java只支持单继承,即一个类只能有一个直接父类。注意,这里说的是只能有一个直接父类,父类的资源也是可以被继承的,相当于父亲从爷爷那里继承的资产,可以传到儿子手上。

子类访问父类

实现了继承关系的父子类,在创建子类对象时,无论调用无参构造还是有参构造,都会默认先创建父类对象,并且是通过父类的无参构造来完成实例化的。

public class Main {
    public static void main(String[] args){
        Tom tom = new Tom();
        Tom tom1 = new Tom(1);

    }
}
public class User {
    public User(){
        System.out.println("调用 * 无参 * 构造创建 - 父类 - 对象");
    }
    public User(int id){
        System.out.println("调用 * 有参 * 构造创建 - 父类 - 对象");
    }
}
public class Tom extends User{
    public Tom(){
        System.out.println("调用 * 无参 * 构造创建 - 子类 - 对象");
    }
    public Tom(int id){
        System.out.println("调用 * 有参 * 构造创建 - 子类 - 对象");
    }
}

// 输出
调用 * 无参 * 构造创建 - 父类 - 对象
调用 * 无参 * 构造创建 - 子类 - 对象
调用 * 无参 * 构造创建 - 父类 - 对象
调用 * 有参 * 构造创建 - 子类 - 对象

在子类实例化对象时,会创建父类对象,那么会不会调用其他类的构造函数,即父类有没有自己的父类。答案是父类也有自己的父类,且也会进行创建。父类的父类不是由我们定义,而是Java提供。

Java中的每个类都有一个共同父类Object,Object类就是所有Java类的根,所有的Java类都是由Object类派生出来的。

public class Main {
    public static void main(String[] args){
        Tom tom = new Tom();
        Tom tom1 = new Tom(1);

    }
}
public class People {
    public People(){
        System.out.println("调用 * 无参 * 构造创建 - 父类 - 对象 people");
    }
    public People(int id){
        System.out.println("调用 * 有参 * 构造创建 - 父类 - 对象 people");
    }
}
public class User extends People{
    public User(){
        System.out.println("调用 * 无参 * 构造创建 - 父类 - 对象");
    }
    public User(int id){
        System.out.println("调用 * 有参 * 构造创建 - 父类 - 对象");
    }
}
public class Tom extends User{
    public Tom(){
        System.out.println("调用 * 无参 * 构造创建 - 子类 - 对象");
    }
    public Tom(int id){
        System.out.println("调用 * 有参 * 构造创建 - 子类 - 对象");
    }
}

//输出
调用 * 无参 * 构造创建 - 父类 - 对象 people
调用 * 无参 * 构造创建 - 父类 - 对象
调用 * 无参 * 构造创建 - 子类 - 对象
调用 * 无参 * 构造创建 - 父类 - 对象 people
调用 * 无参 * 构造创建 - 父类 - 对象
调用 * 有参 * 构造创建 - 子类 - 对象

super关键字

super关键字的作用是:(1)调用父类的有参构造(2)子类访问父类的属性和方法。

public class Main {
    public static void main(String[] args){
        Tom tom = new Tom();
        Tom tom1 = new Tom(1);

    }
}
public class User {
    public User(){
        System.out.println("调用 * 无参 * 构造创建 - 父类 - 对象");
    }
    public User(int id){
        System.out.println("调用 * 有参 * 构造创建 - 父类 - 对象");
    }
}
public class Tom extends User{
    public Tom(){
        System.out.println("调用 * 无参 * 构造创建 - 子类 - 对象");
    }
    public Tom(int id){
        super(id);
        System.out.println("调用 * 有参 * 构造创建 - 子类 - 对象");
    }
}

// 输出
调用 * 无参 * 构造创建 - 父类 - 对象
调用 * 无参 * 构造创建 - 子类 - 对象
调用 * 有参 * 构造创建 - 父类 - 对象
调用 * 有参 * 构造创建 - 子类 - 对象

从上面的代码可以看到,利用super(id)调用了有参构造来创建父类对象,同类调用父类无参构造的代码时super(),并且这种方式是默认设置。

子类普通方法中调用父类的属性和方法。调用父类属性:super.属性名;调用父类方法:super.方法名();

public class Main {
    public static void main(String[] args){
        Tom tom = new Tom();
        tom.show();
    }
}
public class User{
    public String name = "Alice";
    public void show(){
        System.out.println("调用父类中的方法");
    }
}
public class Tom extends User{
    public void show(){
        System.out.println(super.name);
        super.show();
    }
}

// 输出
Alice
调用父类中的方法

子类访问权限

首先介绍权限修饰符,访问权限修饰符可以用来修饰类、属性和方法,不同的访问权限修饰符表示不同的作用域,包括public、protected、默认修饰符和private。一般使用public来修饰类,一般都是说对属性和方法的访问权限修饰符,其作用域如下:

同一个类 同一个包 不同包 子类
public 可以访问 可以访问 可以访问 可以访问
protected 可以访问 可以访问 不能访问 可以访问
默认修饰符 可以访问 可以访问 不能访问 不能访问
private 可以访问 不能访问 不能访问 不能访问

子类只能访问父类public和protected修饰的属性和方法,默认修饰符和private修饰的属性和方法不能访问。

包(package)

包是用来管理Java类,类似于用不同的文件夹管理不同的文件,一个项目中不可避免地会出现同名地Java类,为了防止产生冲突,可以把同名的Java类分别放入不同的包中。

包的作用:(1)管理Java类,便于查找和使用相应的文件;(2)区分同名的类,防止命名冲突;(3)实现访问权限控制

包的命名规范:包名由小写字母组成,不能以.开头或结尾。包名一般由小写字母组成,可以包含数字,但不能以数字开头,使用.来分层。在一个类中调用不同包的类时,需要使用import关键字导入该类,语法:“import 包名.类名”

方法重写

子类在继承父类方法的基础上,对父类的方法重新定义并覆盖的操作叫做方法重写。

public class Main {
    public static void main(String[] args){
        Tom tom = new Tom();
        tom.show();
    }
}
public class User{
    public String name = "Alice";
    public void show(){
        System.out.println("这是父类中的一个方法");
    }
}
public class Tom extends User{
    public void show(){
        System.out.println("这是子类中的一个方法");
    }
}

// 输出
这是子类中的一个方法

// 如果注释子类的重写的方法,输出为
这是父类中的一个方法

方法重写的规则:

  • 父子类的方法名相同;
  • 父子类的方法参数列表相同;
  • 子类方法返回值与父类方法返回值类型相同或者是其子类;
  • 子类方法的访问权限不能小于父类(作用域从大到小排序:public > protected > 默认修饰符 > private)。

父类的静态方法不能被子类重写为非静态方法,父类的非静态方法不能被子类重写为静态方法,父类的私有方法不能被子类重写。

方法重写与方法重载的区别

所在位置 方法名 参数列表 返回值 访问权限
方法重写 子类 相同 相同 相同或者是其子类 不能小于父类
方法重载 同一个类 相同 不同 没有要求 没有要求

多态

多态简单解释就是一个事物有多种表现形态,在Java程序中,就是定义一个方法,在具体的生产环境中根据不同的需求呈现出不同的业务逻辑。

public class OrdinaryMember{
    public void buyBook() {
        System.out.println("普通会员打9折");
    }
}
public class Cashier{
    private OrdinaryMember ordinaryMember;
    // getter、setter方法

    public void settlement() {
        this.ordinaryMember.buyBook();
    }
}

public class Test{
    public static void main(String[] args){
        OrdinaryMember ordinaryMember = new OrdinaryMember();
        Cashier cashier = new Cashier();
        cashier.setOrdinaryMember(ordinaryMember);
        cashier.settlement();
    }
    
}

// 输出
普通会员打9----------------------------------------------

//用户希望买书能享受更大优惠,就把普通会员升级为超级会员,买书可以享受6折优惠。现在用超级会员再次购书,这时候就需要创建超级会员类,并且Cashier类和Test类也需要做相应的修改,如下。
public class SuperMember{
    public void buyBook() {
        System.out.println("超级会员打7折");
    }
}
public class Cashier{
    private SuperMember superMember;
    // getter、setter方法

    public void settlement() {
        this.superMember.buyBook();
    }
}
public class Test{
    public static void main(String[] args){
        SuperMember superMember = new SuperMember();
        Cashier cashier = new Cashier();
        cashier.setSuperMember(superMember);
        cashier.settlement();
    }
    
}

// 输出
超级会员打7

这种方式存在明显不足,当需求发生改变时需要频繁地修改代码,代码的扩展性、维护性较差。使用多态可以进行优化,多态的思路是:创建Member类,作为OrdinaryMember和SuperMember的父类,在OrdinaryMember和SuperMember中分别对父类方法进行重写,在Cashier类中定义Member类型的成员变量,如下所示。

public class Member{
    public void buyBook(){}
}
public class OrdinaryMember extends Member{
    public void buyBook(){
        System.out.println("普通会员打9折");
    }
}
public class SuperMember extends Member{
    public void buyBook(){
        System.out.println("超级会员打7折");
    }
}
public class Cashier{
    private Member Member;
    // getter、setter方法

    public void settlement() {
        this.member.buyBook();
    }
}
public class Test{
    public static void main(String[] args){
        Member member = new OrdinaryMember();
        Cashier cashier = new Cashier();
        cashier.setMember(member);
        cashier.settlement();

        Member member = new SuperMember();
        cashier.setMember(member);
        cashier.settlement();
    }
    
}

// 输出
普通会员打9折
超级会员打7

同样的会员升级,从普通会员升级为超级会员,此时就不需要修改Cashier类,只需要在Test类的main方法中作出修改:“Member member = new SuperMember();”。

如果业务需要扩展,想升级为其他类型的会员,只需要创建对应的会员类并继承Member类,然后在main方法中作出修改,将该会员的实例化对象赋给member即可。这就是多态,从main方法的角度来看这段代码,我们不去定义具体的OrdinaryMember或者SuperMember,而是定义 Member,然后将具体的实例化对象赋给Member,即同一个Member有多种表现形式。

多态的使用

Member member = new SuperMember();,它用于定义父类变量Member,然后将子类SuperMember的实例化对象赋值给member,即父类引用指向子类对象,是多态的具体表现形式。

多态主要有两种表现形式,一种是定义方法时形参为父类,调用方法时引入的参数为子类对象;另一种是定义方法时返回值的数据类型为父类,调用方法时返回子类对象。

// 定义方法时形参为父类,调用方法时引入的参数为子类对象
public class Cashier{
    public void settlement(Member member){
        member.buyBook();
    }
}
public class Test{
    public static void main(String[] args){
        OrdinaryMember ordinaryMember = new OrdinaryMember();
        SuperMember superMember = new SuperMember();
        Cashier cashier = new Cashier();
        cashier.settlement(ordinaryMember);
        cashier.settlement(superMember);
    }
}

// 定义方法时返回值的数据类型为父类,调用方法时返回子类对象
public class Cashier{
    public Member getMember(String name){
        if(name.equals("ordinaryMember")){
            return new OrdinaryMember();
        }else{
            return new SuperMember();
        }
    }
}

抽象方法和抽象类

根据上面多态中的代码可以发现,OrdinaryMember和 SuperMember会分别对 Member的 buyBook()方法进行重写,也就是说无论 Member 的 buyBook()方法里写了什么,最后都会被子类所覆盖,所以 Member的 buyBook()方法体就是无意义的。那么我们就可以只声明buyBook()方法,而不需要定义buyBook()的方法体,这种没有方法体的方法叫作抽象方法。声明抽象方法时需要添加 abstract关键字,如下所示。

public abstract void buyBook();

一旦类中定义了抽象方法,则该类也必须声明为抽象类,需要在类定义处添加abstract关键字

public abstract class Member{
    public abstract void buyBook();
}

抽象类与普通类的区别是抽象类不能被实例化,抽象方法与普通方法的区别是抽象方法没有方法体。抽象类中可以没有抽象方法,但包含了抽象方法的类必须被定义为抽象类。即可以在抽象类中定义普通方法,但是在普通类中不能定义抽象方法。

既然抽象类不能被实例化,抽象方法也没有方法体,那么为什么要创建抽象类和抽象方法呢?到底有什么用呢?抽象类和抽象方法需要结合多态来使用,构建多态的基础是类的继承和方法重写。既然有重写就意味着父类方法只需要声明,不需要具体的实现,具体实现由子类来完成,父类只是一个抽象的概念或者模板。那么就可以把父类定义为抽象类,需要被子类重写的方法定义为抽象方法,并针对这个抽象的概念进行编程。在具体执行时,给父类赋予不同的子类就会实现不同的功能,这就是多态的意义,即抽象类和抽象方法的作用。

继承了抽象类的子类必须重写父类的抽象方法;如果子类也是抽象类,可以不用重写父类的抽象方法。


文章作者: zerollone
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zerollone !
  目录