OOP1:粗通类、对象和方法

OOP1:粗通类、对象和方法

总结了如何声明一个含有成员变量、构造方法、成员方法的类,如何使用类创建一个对象并访问它的变量,调用它的方法,以及其他关于类的杂项。

一、类、方法和变量的修饰符

修饰符在 Java 中无处不在,声明或定义类、方法和变量也常常用到它。可以说,阅读一个 Java 程序时,最先映入眼帘的往往就是修饰符。因此笔者最先介绍修饰符,以免带来理解困难。

1. 概述

Java 修饰符就是用来定义类、方法和变量权限、特性、状态的关键字,相当于给代码加标签,控制它们能被谁访问、怎么使用、生命周期是什么。

Java 的修饰符分为两大类:访问权限修饰符非访问修饰符。前者决定类、方法和变量能不能被其他类使用,后者控制功能、行为、生命周期。

  • 访问权限修饰符包含:

public(公共)

protected(保护)

default(缺省,通常不写)

private(私有)

  • 非访问修饰符包含:

static(静态)

final(最终,表示固定)

abstract(抽象)

  • 修饰符可以组合使用,例如:

public static final(表示全局常量)

2. 访问权限修饰符

(1) public

  • 全局都能访问,所有类(包括其他包)都能使用。

  • 在其他包使用公共类时,需要先用 import 语句导入。

  • 一个 .java 文件中只能有一个用 public 修饰的公共外部类。

外部类:写在文件最外层的类,而内部类则是被嵌套在外部类内部的类。

(2) protected

  • 同一个包内的所有类都能使用,不同包中只有其子类可以使用。
  • 在其他包的子类使用时,需要先用 import 语句导入。

子类:子类通过关键字 extends ,继承父类所有非私有的属性和方法,形如:

class 子类 extends 父类{}

(3) default

  • 缺省型,不写即可。
  • 只有同一个包内的所有类能使用。

(4) private

  • 只有当前类可以使用。
  • 可使用 getter 让外部间接访问一个私有类型的变量。

(5)总结

修饰符 同类 同包不同类 不同包子类 不同包非子类
public
protected ×
default × ×
private × × ×

(6)修饰类、方法和变量的异同

修饰符 修饰外部类 修饰方法 修饰成员变量
public 可用 可用 可用
protected 不能修饰外部类 可用 可用
default 可用 可用 可用
private 不能修饰外部类 可用 可用

局部变量唯一可用的修饰符是 final ,其余一切修饰符均不能修饰局部变量!

3. 非访问修饰符

(1) static

  • 修饰类:只有内部类可用 static。
  • 修饰方法:静态方法,不用 new 对象,直接"类名.方法()"调用,静态方法内不能用 this 关键字。
  • 修饰变量:静态变量,全类共享,所有对象共用同一个值。

(2) final

  • 修饰类:类不能被继承(无子类)。
  • 修饰方法:方法不能被子类重写。
  • 修饰变量:变量变成常量,赋值后不能修改。

(3) abstract

  • 修饰类:抽象类,不能 new 对象,专门用来被继承。
  • 修饰方法:抽象方法,没有方法体,子类必须重写。
  • 不能修饰变量。

二、调用方法、访问变量

轻声呼唤你的名字。

1. Java 中的方法和变量

  • 不同于 C 语言, C 语言中可以在主函数外定义函数和变量,它们看起来"居无定所"。在 Java 中,方法和变量要么属于类,要么属于对象。不能在类外定义方法和变量。

  • 如果成员变量用 static 修饰,则该变量称为静态变量类变量 ,否则称为实例变量

  • 如果成员方法用 static 修饰,则该方法称为静态方法类方法 ,否则称为实例方法

  • 如果一个变量定义在类体内、方法外,用 static 修饰,那么它就是常说的全局变量。它是一种静态变量,属于类本身。

  • 静态变量和静态方法属于类,实例变量和实例方法属于对象(实例)。

  • 若只用 static 方法和 static 变量,不创建对象,数据和方法分离,便能用 Java 写出面向过程风格的程序,适用于实现一些简单的功能。

  • 实例方法能调用实例方法和静态方法,访问实例变量和静态变量;静态方法只能调用静态方法,访问静态变量。

2. 如何调用方法、访问变量

罗列常见的访问方式:直呼其名、对象名、类名、 this 关键字、 super 关键字、 getter 访问方法。

(1)直呼其名

常用于访问、调用同类的变量、方法或访问一个局部变量,无法访问、调用另一个无继承关系的类的任何变量、方法,无论该类是不是公共类,在不在同包。

能访问的变量:

  • 局部变量(最优先)
  • 当前类的实例变量(无歧义时)

如果局部变量与实例变量重名,则直呼其名访问的是局部变量。

  • 当前类的静态变量
  • 从父类继承的非私有变量

能调用的方法:

  • 当前类的实例方法
  • 当前类的静态方法
  • 从父类继承的非私有方法

(2)对象名和类名

  • 实例变量、实例方法必须用对象名访问、调用,形如:对象名.变量名,对象名.方法名。

  • 静态变量、静态方法一般用类名访问、调用,形如:类名.变量名,类名.方法名。

  • 如果用同一个类的不同对象名访问该类下的同一个静态变量,它们引用的实际上是同一个变量。

  • 访问权限由变量、方法前的修饰符决定。

(3)this/super 关键字

  • this 表示当前对象本身,可在实例方法或构造方法访问实例成员和静态成员,有以下用途:

    • 解决局部变量与成员变量同名的问题,形如 this. 变量名。
    • 解决方法参数与成员变量同名的问题,形如 this. 变量名。
    • 重载构造方法时,用来调用该类的另一个构造方法,形如 this([参数列表]),必须写在构造方法的第一排。
  • super 可在子类中使用,用来引用当前对象的父类对象,有以下用途

    • 在子类中调用父类中被覆盖的方法,形如 super. 方法名([参数列表])。
    • 在子类的构造方法中调用父类的构造方法,形如 super([参数列表])。
    • 在子类中访问父类中被隐藏的成员变量,形如 super. 变量名。

this/super 关键字无法在静态方法中使用。

(4)getter访问方法

getter 是类中的一个实例成员方法,通过调用 getter ,返回成员变量,达到访问的目的。通常用这种方法实现对私有型成员变量的访问。

三、面向对象程序设计范式

一般在写 Java 程序时,不会用一个写了主方法的类来实例化对象,即实现业务类启动类的分离。业务类中只定义属性和方法,没有 main 。启动类中只包含主方法,负责创建对象并调用其方法。

1. 业务类-类声明

业务类是实现具体业务逻辑的组件,是应用的核心。我们通常会定义若干业务类,并用业务类创建对象,从而满足业务需求。一个类的定义包括两部分:类声明和类体的定义。

类声明

java 复制代码
[修饰符] class 类名
{
	//1.成员变量
	//2.构造方法
	//3.成员方法
}

使用 class 关键字定义类。花括号内的部分称为类体 。类体中通常包括三部分内容:成员变量成员方法构造方法。构造方法用于创建类实例,成员变量定义对象状态(属性),成员方法定义对象行为。

设计举例

开发一个处理银行业务的应用程序,需要设计一个表示账户的类。一个账户应该有账号、姓名以及余额等属性,它们定义为成员变量。另外,账户应该有取款操作和存款操作,它们定义为成员方法。程序 1-1 定义一个账户类 Account。

程序 1-1 Account.java

java 复制代码
package com.fish4174;//包,用于把功能相关的类和接口分组管理

public class Account
{//次行格式YYDS
    //成员变量的定义,private 进行封装,用 getter/setter 进行访问、修改
    private int id;
    private String name;
    private double balance;//余额

    //构造方法
    public Account(){}//默认构造方法
    public Account(int id, String name, double balance)//构造方法重载:带参数构造方法
    {
        this.id = id;
        this.name = name;
        this.balance = balance;
    }

    //访问方法与修改方法
    public int getId()
    {
        return id;
    }
    public void setId(int id)
    {
        this.id = id;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public double getBalance()
    {
        return balance;
    }
    public void setBalance(double balance)
    {
        this.balance = balance;
    }

    //存款方法
    public void deposit(double amount)
    {
        balance = balance + amount;
        System.out.println("目前账户余额是:" + balance);
    }
    //取款方法
    public void withdraw(double amount)
    {
        balance = balance - amount;
        System.out.println("目前账户余额是:" + balance);
    }
}

编译该程序可得到一个 Account.class 类文件。

2. 启动类

启动类是包含 main 方法的入口类,我们在 main 方法中创建对象、输入参数、调用对象的方法执行操作并得到输出信息。

创建和使用对象

为了使用对象,一般还要声明一个对象名,即声明对象的引用,然后使用 new 运算符调用类的构造方法创建对象。例如,下面两行声明并创建 Account 类的一个实例 myAccount 。

java 复制代码
Account myAccount;
myAccount = new Account();

对象的声明和创建对象可以使用一个语句完成。

java 复制代码
Account myAccount = new Account();

下面程序 4-2 使用 Account 类创建一个对象并访问、调用它的变量和方法。

程序 1-2 AccountDemo.java

java 复制代码
package com.fish4174;

public class AccountDemo
{
    public static void main(String[] args)
    {
        Account myaccount;//声明一个 Account 类型的引用变量
        myaccount = new Account(1001, "Tian Wang", 2000);//调用构造方法创建对象
        myaccount.deposit(5000);//调用对象方法
        myaccount.withdraw(3000);
        //访问对象成员,输出账户信息
        System.out.println("账户ID = " + myaccount.getId());
        System.out.println("姓名 = " + myaccount.getName());
        System.out.println("余额 = " + myaccount.getBalance());
    }
}

运行结果如下。

3. 业务类-类体的定义

(1)成员变量的定义

格式:

[修饰符] 类型 变量名[ = 初值];

(2)构造方法的定义

  • 构造方法也叫构造器,是类的一种特殊方法,作用是创建对象并初始化对象的状态。它和普通方法的区别如下:

    • 构造方法的名称必须与类名相同。
    • 构造方法不能有返回值,也不能返回 void。
    • 构造方法必须在创建对象时用 new 运算符调用。

    格式:

    java 复制代码
    [public/protected/private]类名([参数列表])
    {
    	//方法体
    }
  • 无参数构造方法:如果在定义类时没有为类定义任何构造方法,则编译器自动为类添加一个默认构造方法 。默认构造方法是无参数构造方法,方法体为空。

    • 为 Account 类定义无参数构造方法(亦即自动添加的默认构造方法):

      public Account(){}//默认构造方法,不带参数,方法体为空

    • 创建对象:

      Account myaccount = new Account();

      新建对象的成员变量值都被赋予默认值。

  • 带参数构造方法:可在创建对象时就将其成员变量设置为某个值。一旦为类定义了带参数的构造方法,编译器就不再提供默认构造方法。

    • 创建 Account 对象时指定账户 ID 、姓名和余额:

      java 复制代码
      public Account(int id, String name, double balance)
      {
      	this.id = id;
      	this.name = name;
      	this.balance = balance;
      }
    • 创建对象:

      Account account = new Account(1001, "Tina Wang", 2000);

  • 构造方法的重载:构造方法和普通方法都可以重载,所谓重载是名称相同、参数不同的方法。创建对象时,会根据输入参数的数量和类型不同,调用不同的构造方法。

(3)成员方法的定义

  • 方法用于实现对象的动态特征,也是在对象上可完成的操作。方法必须定义在类体内。

    格式:

    java 复制代码
    修饰符 返回值类型 方法名([形式参数列表])//方法头
    {
    	//方法体
    }

    方法的第一行为方法头 ,包含方法的修饰符、返回值类型、方法名和方法的参数。而方法名、参数个数、参数类型和参数顺序的组合(方法名([形式参数列表]))称为方法签名,它是一个方法区别于其他方法的标签。方法签名将用在方法重载、方法覆盖和构造方法中。

  • 方法的调用:方法的调用主要使用在三种场合:

    • 用对象引用调用类的实例方法。(对象名.方法名)
    • 类中的方法调用本类的其他方法。(直呼其名)
    • 用类名直接调用 static 方法。(类名.方法名)

    要调用类的实例方法,应先创建一个对象,然后通过对象引用调用,如下所示:

    java 复制代码
    Account myAccount = new Account();
    myAccount.deposit(3000);

    如果要调用类的静态方法,通常使用类名调用,如下所示:

    java 复制代码
    double rand = Math.random();
  • 方法重载 :方法重载机制允许在一个类中定义多个同名的方法。实现方法重载,要求同名的方法要么参数个数不同,要么参数类型不同,仅返回值不同不能区分重载的方法。方法重载就是在类中允许定义签名不同的方法

    通过方法重载可实现编译时多态(静态多态),编译器根据参数的不同调用相应的方法,具体调用哪个方法是由编译器在编译阶段静态决定的。

  • 方法参数的传递 :Java 中方法的参数传递是按值传递 ,即在调用方法时将实际参数的值复制传递给方法中的形式参数,方法调用结束后实际参数的值并不改变。形式参数是局部变量,其作用域只在方法内部,离开方法后自动释放。

    基本数据类型 (如 int, double)的参数和引用数据类型(如类、接口、数组)的参数的传递有所不同。

    • 对于基本数据类型的参数,是将实际参数值复制传递给方法,方法调用结束后,对原来的值没有影响。
    • 当参数是引用类型时,实际传递的是引用值,因此在方法的内部有可能改变原来的对象。

    下面程序 1-3 演示了方法参数的两种传递,按值传递和按引用传递。

    程序 1-3 PassByValue.java

    java 复制代码
    package com.fish4174;
    
    public class PassByValue
    {
        public static void changeValue(int num)
        {
            num = 200;
            System.out.println(num);//输出200
        }
        public static void changeValue(Account account0)
        {
            //在方法体中修改账户的余额
            account0.setBalance(10000);
        }
    
        public static void main(String[] args)
        {
            int number = 100;
            changeValue(number);//把number的值100传递给参数num
            System.out.println(number);//输出的number值仍为100,不改变
            Account account = new Account();
            account.setBalance(8000);
            System.out.println(account.getBalance());//输出8000.0
            changeValue(account);//将对象传递给方法,account与参数account0指向同一个对象
            System.out.println(account.getBalance());//输出10000.0
        }
    }

    注意 如果方法传递的是不可改变的引用类型对象(如 String对象),对象在方法内部不可能被改变。

四、杂项

1. UML图

类名


(访问权限)成员变量名1:类型

(访问权限)成员变量名2:类型

(访问权限)成员变量名3:类型


(访问权限)构造方法([参数名1:类型...])

(访问权限)成员方法名1([参数名1:类型...]):返回值类型

(访问权限)成员方法名2([参数名1:类型...]):返回值类型

访问权限说明:+ 代表 public 成员,- 代表 private 成员,# 代表 protected 成员,不加则代表缺省型成员。

2. 对象的引用赋值

对于基本数据类型的变量赋值,是将变量的值的一个复制赋给另一个变量。例如:

java 复制代码
int x = 10;
int y = x;//将x的值10复制给变量y

对象(包括数组等引用数据类型的变量)的赋值是将对象的引用(地址)赋给变量。例如:

java 复制代码
Account account = new Account();
Account account2 = account;//将account的引用赋给account2

上面的赋值语句会使 account 和 account2 指向同一个对象。这时如果修改 account 对象的成员变量值,account2 的对应变量值也会被修改。

3.栈区、堆区、方法区

JVM 将内存大致分为三部分:栈区、堆区、方法区。

栈区:存放局部变量的值、方法的参数值,当变量的生存期结束后,系统自动释放栈区内存。

堆区:存放 new 创建的对象和数组。如果一个对象没有引用指向它,它的内存将被回收。

方法区:存放类的成员方法代码。

4. 成员变量默认值

  • 定义在类体内、方法外的变量即是成员变量,若在类的定义中没有为变量赋初值,编译器会为每个成员变量指定一个默认值。

  • 实例变量的默认值使在创建对象时赋予的,静态变量的默认值是在类首次加载时赋予的,且全局只有一份。

引用数据类型 的变量默认值为 null,基本数据类型的变量默认值如下表:

变量类型 初始值 变量类型 初始值
byte 0 float 0.0F
short 0 double 0.0D
int 0 boolean false
long 0L char \u0000

5. 局部变量类型推断(var)

在局部变量的类型可推断时,可以用 var 声明之,不必写出具体的类型名。使用 var 进行声明时必须同时赋值(初始化),否则编译器无法推断类型。

应用场景:

  • 简化对象的创建

    java 复制代码
    var myAccount = new Account();

    下面的语句是错误的。

    java 复制代码
    var myAccount;//错误,此时不能推断myAccount的类型
  • 字符串也是引用类型,可用 var

    java 复制代码
    var name = "Tina Wang";
  • 数组也是引用类型,可用 var

    java 复制代码
    var nums = new int[]{0, 1, 2};

    下面的语句是错误的。

    java 复制代码
    var nums = {0, 1, 2};//错误,不能推断元素类型
  • for 循环和增强的 for 循环中也可以用 var 声明变量。

    java 复制代码
    for(var i = 0; i < number.length; i++)
    {
    	System.out.print(number[i] + " ");
    }
    for(var i : number)
    {
    	System.out.print(i + " ");
    }

6. 垃圾回收

对象名存放的是对象的引用。当对象不再被引用时,该对象就会被销毁,其所占的内存空间也会被释放,这个过程称为垃圾回收。这个操作是在后台自动进行的。

对象何时有可能被回收

  • 当一个对象不再被引用时,该对象才有可能被回收。例如:

    java 复制代码
    Account account = new Account();
    Account account2 = new Account();
    account2 = account;

    上面的代码段创建了两个 Account 对象 account、account2 ,然后让 account2 指向 account,这时 account2 原来指向的对象没有任何引用指向它了,也没有任何办法得到或操作该对象了,该对象就有可能被回收了。

  • 通过为对象引用赋 null 值,可以明确删除一个对象的引用。例如

java 复制代码
account2 = null;//原来的account2对象可被回收,注意与上面代码的区别
  • 一个对象可能有多个引用,只有在所有的引用都被删除时,对象才有可能被回收。