Skip to content

基础

学习面向对象内容的三条主线:属性(重点)、方法、构造器、代码块(熟悉)、内部类。

面向对象的特征:封装、继承、多态、抽象。

其他关键字的使用:this、super、package、import、static、final、interface、abstract 等。

面向对象编程概述(了解)

程序设计的思路

面向对象,是软件开发中的一类编程风格、开发范式。除了面向对象,还有面向过程、指令式编程和函数式编程。 在所有的编程范式中,我们接触最多的还是面向过程和面向对象两种。

早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显,出现了面向对象思想并成为目前主流的方式。

面向过程编程(Process Oriented Programming),简称 POP。

oriented,面向,朝向的意思。

关注的焦点是过程。过程就是操作数据的步骤。如果某个过程的实现代码重复出现,那么就可以把这个过程抽取为一个函数。 这样就可以大大简化冗余代码,便于维护。

代码结构以函数为组织单位。是一种“执行者思维”,适合解决简单问题。扩展能力差、后期维护难度较大。

面向对象编程(Object Oriented Programming),简称 OOP。

关注的焦点是类。在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。

代码结构以类为组织单位。每种事物都具备自己的属性、行为和功能。 是一种“设计者思维”,适合解决复杂问题。代码扩展性强、可维护性高。

我们千万不要把面向过程和面向对象对立起来。他们是相辅相成的。面向对象离不开面向过程!

Java 语言的基本元素:类和对象

类和对象概述

类(Class)和对象(Object)是面向对象的核心概念。

什么是类?

具有相同特征的事物的抽象描述,是 抽象的、概念上的定义。

什么是对象?

实际存在的该类事物的每个个体,是具体的,因而也称为实例(instance)。

可以这样理解:

类 => 抽象概念的人
      
对象 => 实实在在的某个人

类的成员概述

Java 中的类是一组相关属性和行为的集合,这也是类最基本的两个成员。

成员变量 <=> 属性 <=> Field
成员方法 <=> 函数 <=> Method

面向对象完成功能的三步骤(重要)

步骤 1:类的定义

类的定义使用关键字:class。格式如下:

[修饰符] class 类名{
    属性声明;
    方法声明;
}

举例:

java
public class Person {
    // 声明属性 age
    int age;

    // 声明方法 showAge()
    public void eat() {
        System.out.println("人吃饭");
    }
}
java
public class Dog {
    // 声明属性
    String type; // 种类
    String nickName; // 昵称
    String hostName; // 主人名称

    // 声明方法
    public void eat() { // 吃东西
        System.out.println("狗狗进食");
    }
}
java
public class Person {
    String name;
    char gender;
    Dog dog;

    // 喂宠物
    public void feed() {
        dog.eat();
    }
}

步骤 2:对象的创建

创建对象,使用关键字 new。创建对象语法:

// 方式 1:给创建的对象命名
// 把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了
类名 对象名 = new 类名();

// 方式 2:
new 类名() // 也称为匿名对象

举例:

java
class PersonTest {
    public static void main(String[] args) {
        // 创建 Person 类的对象
        Person per = new Person();
        // 创建 Dog 类的对象
        Dog dog = new Dog();
    }
}

步骤 3:对象调用属性或方法

对象是类的一个实例,必然具备该类事物的属性和行为(即方法), 使用对象名.属性对象名.方法的方式访问对象成员(包括属性和方法)。

举例:

java
// 声明 Animal 类
public class Animal { // 动物类
    public int legs;

    public void eat() {
        System.out.println("Eating.");
    }

    public void move() {
        System.out.println("Move.");
    }
}

// 声明测试类
public class AnimalTest {
    public static void main(String args[]) {
        // 创建对象
        Animal xb = new Animal();
        xb.legs = 4; // 访问属性
        System.out.println(xb.legs);
        xb.eat(); // 访问方法
        xb.move(); // 访问方法
    }
}

匿名对象(anonymous object)

我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。

如:new Person().shout();

使用情况:

  • 如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象
  • 我们经常将匿名对象作为实参传递给一个方法调用

对象的内存解析

JVM 内存结构划分

HotSpot Java 虚拟机的架构图如下。其中我们主要关心的是运行时数据区部分(Runtime Data Area)。

  • 堆(Heap):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一 点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  • 栈(Stack):是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各 种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类 型,它不等同于对象本身,是对象在堆内存的首地址)。方法执行完,自动释放。
  • 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的 代码等数据。

对象内存解析

java
class Person {
    String name;
    int age;
    boolean isMale;
}

public class PersonTest { //测试类
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "赵同学";
        p1.age = 20;
        p1.isMale = true;
        Person p2 = new Person();
        p2.age = 10;
        Person p3 = p1;
        p3.name = "郭同学";
    }
}

内存解析图:

说明

  • 凡是 new 出来的结构(对象、数组)都放在堆空间中
  • 对象的属性存放在堆空间中
  • 创建一个类的多个对象(比如 p1、p2),则每个对象都拥有当前类的一套副本。当通过一个对象修改其属性时,不会影响其它对象此属性的值
  • 当声明一个新的变量使用现有的对象进行赋值时(比如 p3 = p1),此时并没有在堆空间中创建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时,会影响另外一个对象对此属性的调用

面试题:对象名中存储的是什么呢?

答:对象地址

java
public class StudentTest {
    public static void main(String[] args) {
        System.out.println(new Student()); // Student@7852e922
        Student stu = new Student();
        System.out.println(stu); // Student@4e25154f

        int[] arr = new int[5];
        System.out.println(arr); // [I@70dea4e
    }
}

类的成员之一:成员变量(field)

如何声明成员变量

语法格式:

[类修饰符] class 类名{
    [成员变量修饰符] 数据类型 成员变量名 [= 初始化值]; // [!code hl]
}

说明:

  • 成员变量的位置要求必须在类中,方法外
  • 成员变量常用的权限修饰符有 private、缺省、protected、public,还有 static、final
  • 可以是任意数据类型
  • 属于标识符,符合命名规则和规范即可
  • 根据情况,可以显式赋值,也可以不赋值使用默认值

示例:

java
public class Person {
    private int age; // 声明 private 变量 age
    public String name = "Lila"; // 声明并初始化 public 变量 name
}

成员变量 vs 局部变量

变量的分类

变量可以分为成员变量与局部变量。

在方法体外,类体内声明的变量称为成员变量;在方法体内部等位置声明的变量称为局部变量。

  • 所有变量
    • 成员变量
      • 实例变量(不以 static 修饰)
      • 类变量(以 static 修饰)
    • 局部变量
      • 形参(方法、构造器中定义的变量)
      • 方法局部变量(在方法内定义)
      • 代码块局部变量(在代码块内定义)

static 可以将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。接下来先学习实例变量。

对比

相同点:

  • 变量声明的格式相同:数据类型 变量名 = 初始化值
  • 变量必须先声明、后初始化、再使用
  • 变量都有其对应的作用域,只在其作用域内是有效的

不同点:

1、声明位置和方式

  • 实例变量:在类中方法外
  • 局部变量:在方法体 {} 中或方法的形参列表、代码块中

2、在内存中存储的位置不同

  • 实例变量:堆
  • 局部变量:栈

3、生命周期

  • 实例变量:和对象的生命周期一样,随着对象的创建而存在,随着对象被 GC 回收而消亡,而且每一个对象的实例变量是独立的
  • 局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随着方法执行的结束而消亡,而且每一次方法调用都是独立

4、作用域

  • 实例变量:通过对象就可以使用,本类中直接调用,其他类中通过对象.实例变量形式调用
  • 局部变量:出了作用域就不能使用

5、修饰符

  • 实例变量:public、protected、private、final、volatile、transient 等
  • 局部变量:final

6、默认值

  • 实例变量:有默认值
  • 局部变量:没有,必须手动初始化。其中的形参比较特殊,靠实参给它初始化

TIP

动态初始化数组的元素和实例变量才有默认值,局部变量必须手动初始化,否则编译会提示报错。

对象属性的默认初始化赋值

成员变量类型初始值
byte数字 0
short数字 0
int数字 0
long数字 0L
float数字 0.0F
double0.0
char'\u0000'
booleanfalse
引用类型null

举例

java
class Person { // 人类
    // 1.属性
    String name; // 姓名
    int age = 1; // 年龄
    boolean isMale; // 是否是男性

    public void show(String nation) {
        // nation: 局部变量
        String color; // color: 局部变量
        color = "yellow";
    }
}

// 测试类
class PersonTest {
    public static void main(String[] args) {
        Person p = new Person();
        p.show("CHN");
    }
}

类的成员之二:方法(method)

方法(method、函数)的理解

  • 方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。
  • 将功能封装为方法的目的是实现代码重用,减少冗余,简化代码
  • Java 里的方法不能独立存在,所有的方法必须定义在类里

举例:

  • Math.random()random()
  • Math.sqrt(x)sqrt(x)
  • System.out.println(x)println(x)
  • new Scanner(System.in).nextInt()nextInt()
  • Arrays 类中的 binarySearch()sort()equals()
java
public class Person {
    private int age;

    public int getAge() {  // 声明方法 getAge()
        return age;
    }

    public void setAge(int i) {  // 声明方法 setAge
        age = i;        // 将参数 i 的值赋给类的成员变量 age
    }
}

如何声明方法

声明方法的语法格式

[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
    方法体的功能代码
}

(1)一个完整的方法 = 方法头 + 方法体

  • 方法头就是[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表],也称为方法签名。通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式
  • 方法体就是方法被调用后要执行的代码。对于调用者来说,不了解方法体如何实现的,并不影响方法的使用

(2)方法头可能包含 5 个部分

  • 修饰符:可选的。方法的修饰符也有很多,例如 public、protected、private、static、abstract、native、final、synchronized 等
    • 其中,权限修饰符有 public、protected、private
    • 其中,根据是否有 static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法
  • 返回值类型:表示方法运行的结果的数据类型,方法执行后将结果返回到调用者
    • 无返回值,则声明为 void
    • 有返回值,则声明出相应的返回值类型。
  • 方法名:属于标识符,命名时遵循标识符命名规则和规范
  • 形参列表:表示完成方法体功能时需要外部提供的数据列表。可以包含零个,一个或多个参数
    • 无论是否有参数,括号 () 不能省略
    • 如果有参数,每一个参数都要指定数据类型和参数名,多个参数之间使用逗号分隔
    • 参数的类型可以是基本数据类型、引用数据类型
    • 可以声明可能 throws 的异常列表

(3)方法体:方法体必须有 {} 括起来,在 {} 中编写完成方法功能的代码

(4)关于方法体中 return 语句的说明

  • return 语句的作用是结束方法的执行,并将方法的结果返回去
  • 如果返回值类型不是 void,方法体中必须保证一定有 return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容
  • 如果返回值类型为 void 时,方法体中可以没有 return 语句,如果要用 return 语句提前结束方法的执行,那么 return 后面不能跟返回值,直接写 return; 就可以了
  • return 语句后面就不能再写其他代码了,否则会报错:Unreachable code

如何调用实例方法

方法通过方法名被调用,且只有被调用才会执行。

1、方法调用语法格式

对象.方法名([实参列表])

实例方法可以直接调用其它的实例方法。

java
public class Main {
    public static void main(String[] args) {

        javacode.base.Main main = new javacode.base.Main();
        main.say1();
    }

    static void sayBye() {
        System.out.println("bye bye");
    }

    void say1() {
        System.out.println("说一");
        say2();
    }

    void say2() {
        System.out.println("不二");
    }
}

方法调用内存分析

  • 方法没有被调用的时候,都在方法区中的字节码文件(.class 文件)中存储
  • 方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值
  • 当方法执行结束后,会释放该内存,称为出栈。如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令
  • 方法进出栈的顺序是先进后出,后进先出

练习

练习 1

创建一个 Person 类,其定义如下:

Person

    name: String
    age: int
    sex: int
    
    +study(): void
    +showAge(): void
    +addAge(int i): int

要求:

  1. 创建 Person 类的对象,设置该对象的 name、age 和 sex 属性,调用 study 方法, 输出字符串 studying,调用 showAge() 方法显示 age 值, 调用 addAge() 方法给对象的 age 属性值增加 2 岁。
  2. 创建第二个对象,执行上述操作,体会同一个类的不同对象之间的关系。
java
package javacode.oop1;


public class OopExer1 {
    String name;
    int age;
    int sex;
    public static void main(String[] args) {
        OopExer1 oopExer1 = new OopExer1();
        oopExer1.study();
        oopExer1.addAge(18);
        oopExer1.name = "小花";
        oopExer1.sex = 1;
        System.out.println(oopExer1.sex == 1 ? "男" : "女");
        System.out.println(oopExer1.age);
        System.out.println(oopExer1.name);
        oopExer1.showAge();
    }
    OopExer1 () {
        System.out.println("构造器执行");
    }
    void study() {
        System.out.println("studying");
    }
    void showAge() {
        System.out.println("我今年" + age + "岁啦");
    }
    int addAge(int i) {
        age += i;
        return age;
    }
}

练习 2

利用面向对象的编程方法,设计圆类 Circle,包含属性(半径)和计算圆面积的方法。 定义测试类,创建该 Circle 类的对象,并进行测试。

java
package javacode.oop1;
public class OopExer2 {
    public static void main(String[] args) {
        Circle circle = new Circle();
        circle.radius = 10;
        System.out.println(circle.getCircleS());
    }
}

class Circle {
    int radius; // 半径

    double getCircleS() {
        return Math.PI * radius * radius;
    }
}

练习 3

  1. 编写程序,声明一个 method 方法,在方法中打印一个 10*8 的 * 型矩形,在 main 方法中调用该方法
  2. 修改上一个程序,在 method 方法中,除打印一个 10*8 的 * 型矩形外,再计算该矩形的面积,并将其作为方法返回值。 在 main 方法中调用该方法,接收返回的面积值并打印
  3. 修改上一个程序,在 method 方法提供 m 和 n 两个参数,方法中打印一个 m*n 的 * 型矩形,并计算该矩形的面积,将其作为方法返回值。 在 main 方法中调用该方法,接收返回的面积值并打印
java
package javacode.oop1;

public class OopExer3 {
    public static void main(String[] args) {
        OopExer3 oopExer3 = new OopExer3();
        int s = oopExer3.logStar();
        System.out.println("面积为:" + s);

        int s2 = oopExer3.logStarPlus(2, 3);
        System.out.println("s2 的面积为 " + s2);
    }
    int logStar() {
        int[][] starArr = new int[8][10];
        for(int i = 0; i < starArr.length; i ++) {
            for(int j = 0; j < starArr[i].length; j ++) {
                System.out.print("* ");
            }
            System.out.println();
        }
        int heigth = starArr.length;
        int width = starArr[0].length;
        return width * heigth;
    }

    int logStarPlus(int m, int n) {
        int[][] starArr = new int[m][n];
        for(int i = 0; i < starArr.length; i ++) {
            for(int j = 0; j < starArr[i].length; j ++) {
                System.out.print("* ");
            }
            System.out.println();
        }
        int heigth = starArr.length;
        int width = starArr[0].length;
        return width * heigth;
    }
}

练习 4

声明一个日期类型 MyDate。有属性:年 year,月 month,日 day。创建 2 个日期对象,分别赋值为你的出生日期,你对象的出生日期,并显示信息。

java
package javacode.oop1;

public class OopExer4 {
    public static void main(String[] args) {
        MyDate date1 = new MyDate();
        date1.year = 2001;
        date1.month = 5;
        date1.day = 4;

        MyDate date2 = new MyDate();
        date2.year = 1998;
        date2.month = 12;
        date2.day = 30;

        date1.showInfo();
        date2.showInfo();
    }
}

class MyDate {
    int year;
    int month;
    int day;

    void showInfo() {
        System.out.println(
                "年为 " + year + "," +
                        "月为 " + month + "," +
                        "日为 " + day
        );
    }
}

练习 5

用面向对象的方式编写用户登录程序。

用户类:

  • 属性:用户名,密码
  • 方法:登录

界面类:

  • 在界面类中添加 main 方法,接受用户输入,并调用用户类的登录方法进行验证
  • 输出:
    • 登录失败:用户名或密码错误!
    • 登录成功:欢迎你,用户名!
java
package javacode.oop1;
import java.util.Scanner;

public class OopExer5 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入用户名!");
        String username = scanner.next();
        System.out.println("请输入密码!");
        String password = scanner.next();
        User user = new User();
        boolean login = user.login(username, password);
        scanner.close();
    }
}

class User {
    String username = "lukecheng";
    String password = "133850950";
    boolean login(String un, String pw) {
        if (username.equals(un) && password.equals(pw)) {
            System.out.println("欢迎您,lukecheng!");
            return true;
        } else {
            System.out.println("用户名或密码错误!");
            return false;
        }
    }
}

对象数组

数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用类型中的类时,我们称为对象数组。

练习 0

定义类 Student,包含三个属性:学号 number(int),年级 state(int),成绩 score(int)。创建 20 个学生对象, 学号为 1 到 20,年级和成绩都由随机数确定。

问题一:打印出 3 年级(state 值为 3)的学生信息。

问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息。

java
package javacode.oop1;
public class OopExer6 {
    public static void main(String[] args) {
        Student[] students = new Student[50];
        for (int i = 0; i < students.length; i++) {
            students[i] = new Student(i + 1);
            // 打印 3 年级的学生信息
            if (students[i].state == 3) {
                students[i].logInfo();
            }
        }
        System.out.println("=======");
        System.out.println("=======");
        System.out.println("=======");
        // 使用冒泡排序将学生成绩按从大到小排序,并遍历所有学生信息
        for (int i = students.length - 1; i > 0 ; i--) {
            for (int j = 0; j < i; j++) {
                Student s1 = students[j];
                Student s2 = students[j + 1];
                if (s1.score < s2.score) {
                    students[j] = s2;
                    students[j + 1] = s1;
                }
            }
        }
        for (int i = 0; i < students.length; i++) {
            students[i].logInfo();
        }
    }
}

class Student {
    int number; // 学号
    short state; // 年级
    int score; // 成绩

    Student(int numberParam) {
        // 设置学号
        number = numberParam;
        // 设置年级(1 - 7)
        state = (short) ((Math.random() * 7) + 1);
        // 设置成绩(0 - 100)
        score = (int) (Math.random() * 101);
    }
    void logInfo () {
        System.out.println("我的学号是 " + number + ",我读" + state + "年级,期末考试 " + score + " 分");
    }
}

注意

对象数组,首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象。如果不创建, 数组的元素的默认值就是 null,所以很容易出现空指针异常 NullPointerException。

练习 1

定义矩形类 Rectangle,包含长、宽属性,area() 为返回矩形面积的方法,perimeter() 为返回矩形周长的方法, String getInfo() 为返回圆对象的详细信息(如:长、宽、面积、周长等数据)的方法。

在测试类中创建长度为 3 的 Rectangle[] 数组,用来装 3 个矩形对象,并给 3 个矩形对象的长分别赋值为 10,20,30,宽分别赋值为 5,15,25, 遍历输出。

java
package javacode.oop1;

public class OopExer7 {
    public static void main(String[] args) {
        Rectangle[] rectangles = new Rectangle[3];
        for (int i = 0; i < rectangles.length; i++) {
            rectangles[i] = new Rectangle();
        }
        rectangles[0].length = 10;
        rectangles[1].length = 20;
        rectangles[2].length = 30;

        rectangles[0].width = 5;
        rectangles[1].width = 15;
        rectangles[2].width = 25;

        for (int i = 0; i < rectangles.length; i++) {
            String info = rectangles[i].getInfo();
            System.out.println(info);
        }
    }
}

class Rectangle {
    int length;
    int width;
    // 求面积
    int area() {
        return  length * width;
    }
    // 求周长
    int perimeter() {
        return 2 * (length + width);
    }
    // 返回详细信息
    String getInfo() {
        int s = area();
        int per = perimeter();
        return "该矩形长为 " + length +
                ",该矩形宽为 " + width +
                ",该矩形面积为 " + s +
                ",该矩形周长为 " + per
                ;
    }
}

再谈方法

方法的重载

  • 方法重载:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。参数列表不同,意味着参数个数或参数类型的不同。
  • 重载的特点:与修饰符、返回值类型无关,只看参数列表,且参数列表(参数个数或参数类型)必须不同。调用时,根据方法参数列表的不同来区别。
  • 重载方法调用:JVM 调用方法的参数列表,调用匹配的方法。现在个数、类型最匹配的,再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错。

举例 1:

java
// System.out.println() 方法就是典型的重载方法,其内部的声明形式如下:
public class PrintStream {
    public void println(byte x);

    public void println(short x);

    public void println(int x);

    public void println(long x);

    public void println(float x);

    public void println(double x);

    public void println(char x);

    public void println(double x);

    public void println();
}

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(3);
        System.out.println(1.2f);
        System.out.println("hello!");
    }
}

举例 2:

java
// 返回两个整数的和
public int add(int x,int y){
        return x+y;
        }
// 返回三个整数的和
public int add(int x,int y,int z){
        return x+y+z;
        }
// 返回两个小数的和
public double add(double x,double y){
        return x+y;
        }

举例 3,方法的重载和返回值类型无关。

java
public class MathTools {
    // 以下方法不是重载,会报错
    public int getOneToHundred() {
        return (int) (Math.random() * 100);
    }

    public double getOneToHundred() {
        return Math.random() * 100;
    }
}

可变个数的形参

在 JDK 5.0 中提供了 Varargs(variable number of arguments) 机制。即当定义一个方法时,形参的类型可以确定, 但是形参的个数不确定,那么可以考虑使用可变个数的形参。

格式:

方法名(参数的类型名 ...参数名)

举例:

// JDK 5.0 以前:采用数组形参来定义方法,传入多个同一类型变量
public static void test(int a ,String[] books);
// JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a ,String...books);

特点:

  1. 可变参数:方法参数部分指定类型的参数个数是可变多个:0 个,1 个或多个
  2. 可变个数形参的方法与同名的方法之间,彼此构成重载
  3. 可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错
  4. 方法的参数部分有可变形参,需要放在形参声明的最后
  5. 在一个方法的形参中,最多只能声明一个可变个数的形参

案例 1

n 个字符串进行拼接,每一个字符串之间使用某字符进行拼接,如果没有传入字符串,那么返回空字符串 ""。

java
package javacode.oop1;

public class OopExer8 {
    public static void main(String[] args) {
        StringTools tools = new StringTools();
        System.out.println(tools.concat('-'));
        System.out.println(tools.concat('-',"hello"));
        System.out.println(tools.concat('-',"hello","world"));
        System.out.println(tools.concat('-',"hello","world","java"));
    }
}

class StringTools {
    String concat(char seperator, String... args){
        String str = "";
        for (int i = 0; i < args.length; i++) {
            if(i==0){
                str += args[i];
            }else{
                str += seperator + args[i];
            }
        }
        return str;
    }
}

案例 2

求 n 个整数的和。

java
package javacode.oop1;

public class OopExer9 {
    public static void main(String[] args) {
        NumberTools tools = new NumberTools();
        // 可变形参可以不传参数
        System.out.println(tools.totalV()); // 0 个实参
        System.out.println(tools.totalV(5)); // 1 个实参
        System.out.println(tools.totalV(5,6,2,4)); // 4 个实参
        // 如果传数组,那就不能传第二个参数。这样和数组形式接收参数是一样的。
        System.out.println(tools.totalV(new int[]{5,6,2,4})); // 传入数组实参
        System.out.println("------------------------------------");
        System.out.println(tools.total(new int[]{})); // 0 个元素的数组
        System.out.println(tools.total(new int[]{5})); // 1 个元素的数组
        System.out.println(tools.total(new int[]{5,6,2,4})); // 传入数组实参
    }
}

class NumberTools {
    public int total(int[] nums){
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        return sum;
    }
    public int totalV(int... nums){
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        return sum;
    }
}

案例 3

如下的方法彼此构成重载。

java
package javacode.oop1;

public class OopExer10 {

}

class MathTools {
    // 求两个整数的最大值
    public int max(int a, int b) {
        return a > b ? a : b;
    }
    // 求两个小数的最大值

    public double max(double a, double b) {
        return a > b ? a : b;
    }

    // 求三个整数的最大值
    public int max(int a, int b, int c) {
        return max(max(a, b), c);
    }

    // 求 n 个整数的最大值
    public int max(int... nums) {
        int max = nums[0]; // 如果没有传入整数,或者传入 null,这句代码会报异常
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
        }
        return max;
    }
    // 求 n 整数的最大值
//    public int max(int[] nums){  // 编译就报错,与 (int... nums) 无法区分
//        int max = nums[0]; // 如果没有传入整数,或者传入 null,这句代码会报异常
//        for (int i = 1; i < nums.length; i++) {
//            if(nums[i] > max){
//                max = nums[i];
//            }
//        }
//        return max;
//    }
    // 求 n 整数的最大值
//    public int max(int first, int... nums){  // 当前类不报错,但是调用时会引起多个方法同时匹配
//        int max = first;
//        for (int i = 0; i < nums.length; i++) {
//            if(nums[i] > max){
//                max = nums[i];
//            }
//        }
//        return max;
//    }
}

方法的参数传递机制

形参和实参

  • 形参(formal parameter):在定义方法时,方法名后面括号 () 中声明的变量称为形式参数,简称形参。
  • 实参(actual parameter):在调用方法时,方法名后面括号 () 中的使用的值/变量/表达式称为实际参数,简称实参。

参数传递机制:值传递

Java 里方法的参数传递方式只有一种:值传递。即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

  • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
  • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

练习 1

判断如下程序输出的结果。

java
package javacode.oop1;

public class OopExer11 {
    public void swap(MyData my){
        my = new MyData(); // 考虑堆空间此新创建的对象,和 main 中的 data 对象是否有关
        int temp = my.x;
        my.x = my.y;
        my.y = temp;
    }

    public static void main(String[] args) {
        OopExer11 tools = new OopExer11();

        MyData data = new MyData();
        data.x = 1;
        data.y = 2;
        System.out.println("交换之前:x = " + data.x +", y = " + data.y);
        tools.swap(data); // 调用完之后,x 与 y 的值交换?
        System.out.println("交换之后:x = " + data.x +", y = " + data.y);
    }
}
class MyData{
    int x ;
    int y;
}

练习 2

如下操作是否可以实现数组排序。

java
package javacode.oop1;

public class OopExer12 {
    // 冒泡排序,实现数组从小到大排序
    public void sort(int[] arr){
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
    // 打印数组的元素
    public void print(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]+" ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        OopExer12 tools = new OopExer12();
        int[] nums = {4,3,1,6,7};
        System.out.println("排序之前:");
        tools.print(nums);
        tools.sort(nums);// 对 nums 数组进行排序
        System.out.println("排序之后:");
        tools.print(nums);// 输出 nums 数组的元素
    }
}

练习 3

写出如下程序的输出结果。

java
package javacode.oop1;

public class OopExer13 {
    public static void main(String args[]) {
        OopExer13 test = new OopExer13();
        test.first();
    }
    public void first() {
        int i = 5;
        ValueP v = new ValueP();
        v.i = 25;
        second(v, i);
        System.out.println(v.i);
    }
    public void second(ValueP v, int i) {
        i = 0;
        v.i = 20;
        ValueP val = new ValueP();
        v = val;
        System.out.println(v.i + " " + i);
    }
}

class ValueP {
    int i = 15;
}

练习 4

要求如下:

java
public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 10;
        // 需要在 method 方法被调用之后,仅打印出 a=100, b=200,请写出 method 方法的代码
        method(a, b);
        System.out.println("a=" + a);
        System.out.println("b=" + b);
    }
}

解:

java
    // 法一:
public static void method(int a,int b){
        // 在不改变原本题目的前提下,如何写这个函数才能在 main 函数中输出 a=100,b=200? 
        a=a*10;
        b=b*20;
        System.out.println(a);
        System.out.println(b);
        System.exit(0);
        }
// 法二:
public static void method(int a,int b){
        PrintStream ps=new PrintStream(System.out){
@Override
public void println(String x){
        if("a=10".equals(x)){
        x="a=100";
        }else if("b=10".equals(x)){
        x="b=200";
        }
        super.println(x);
        }
        };
        System.setOut(ps);
        }

练习 5

将对象作为参数传递给方法。

(1)定义一个 Circle 类,包含一个 double 型的 radius 属性,其代表圆的半径,一个 findArea() 方法,返回圆的面积。

(2)定义一个类 PassObject,在类中定义一个方法 printAreas(),该方法的定义如下:public void printAreas(Circle c, int time), 在 printAreas 方法中打印输出 1 到 time 之间的每个整数半径值,以及对应的面积。 例如,times 为 5,则输出半径 1,2,3,4,5,以及对应的圆面积。

(3)在 main 方法中调用 printAreas() 方法,调用完毕后输出当前半径值。

java
package javacode.oop1;

public class OopExer14 {
    public static void main(String[] args) {
        PassObject passObject = new PassObject();
        passObject.printAreas(new Circle2(), 6);
    }
}

class Circle2 {
    double radius; // 半径
    double findArea() {
        return Math.PI * radius * radius;
    }
}

class PassObject {
    public void printAreas(Circle2 c, int time) {
        for (int i = 0; i < time; i++) {
            int tempR = i + 1;
            c.radius = tempR;
            double s = c.findArea();
            System.out.println("半径为 " + tempR + ",面积为 " + s);
        }
    }
}

递归方法(recursion)

方法自己调用自己的现象就称为递归。

递归分为直接递归和间接递归。

  • 直接递归:方法自身调用自己
java
public void methodA(){
        methodA();
        }
  • 间接递归:可以理解为 A() 方法调用 B() 方法,B() 方法调用 C() 方法,C() 方法调用 A() 方法
java
public static void A(){
        B();
        }
public static void B(){
        C();
        }
public static void C(){
        A()
        }

说明

  • 递归方法包含了一种隐式的循环
  • 递归方法会重复执行某段代码,但这种重复执行无须循环控制
  • 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环,最终发生栈内存溢出

举例 1

计算 1 ~ n 的和。

java
package javacode.oop1;

public class OopExer15 {
    public static void main(String[] args) {
        OopExer15 demo = new OopExer15();
        // 计算 1~num 的和,使用递归完成
        int num = 5;
        // 调用求和的方法
        int sum = demo.getSum(num);
        // 输出结果
        System.out.println(sum);

    }

    public int getSum(int num) {
        if (num == 1) {
            return 1;
        }
        return num + getSum(num - 1);
    }
}

举例 2

递归方法计算 n!。

java
package javacode.oop1;

public class OopExer16 {
    public static void main(String[] args) {
        OopExer16 demo = new OopExer16();
        // 计算 1~num 的和,使用递归完成
        int num = 5;
        // 调用求和的方法
        int sum = demo.multiply(num);
        // 输出结果
        System.out.println(sum);

    }

    public int multiply(int num){
        if(num == 1){
            return 1;
        }else{
            return num * multiply(num - 1);
        }
    }

}

举例 3

已知有一个数列:f(0) = 1,f(1) = 4,f(n+2) = 2 * f(n+1) + f(n),其中 n 是大于 0 的整数,求 f(10) 的值。

java
public int f(int num){
    if(num==0){
        return 1;
    }else if(num==1){
        return 4;
    }else{
        return 2*f(num-1)+f(num-2);
    }
}

举例 4

已知一个数列:f(20) = 1,f(21) = 4,f(n+2) = 2*f(n+1)+f(n),其中 n 是大于 0 的整数,求 f(10) 的值。

java
public int func(int num){
    if(num == 20){
        return 1;
    }else if(num == 21){
        return 4;
    }else{
        return func(num + 2) - 2 * func(num + 1);
    }
}

举例 5

计算斐波那契数列(Fibonacci)的第 n 个值,斐波那契数列满足如下规律:

1,1,2,3,5,8,13,21,34,55,....

即从第三个数开始,一个数等于前两个数之和。假设 f(n) 代表斐波那契数列的第 n 个值,那么 f(n) 满足:

f(n) = f(n-2) + f(n-1);

使用递归的写法:

java
int f(int n) { // 计算斐波那契数列第 n 个值是多少
    if (n < 1) { // 负数是返回特殊值 1,表示不计算负数情况
        return 1;
    }
    if (n == 1 || n == 2) {
        return 1;
    }
    return f(n - 2) + f(n - 1);
}

不用递归:

java
int fValue(int n) { // 计算斐波那契数列第 n 个值是多少
        if (n < 1) {//负数是返回特殊值 1,表示不计算负数情况
            return 1;
        }
        if (n == 1 || n == 2) {
            return 1;
        }
        // 从第三个数开始,n 等于前两个整数相加
        int beforeBefore = 1; // 相当于 n=1 时的值
        int before = 1; // 相当于 n=2 时的值
        int current = beforeBefore + before; // 相当于 n=3 的值
        // 再完后
        for (int i = 4; i <= n; i++) {
            beforeBefore = before;
            before = current;
            current = beforeBefore + before;
        }
        return current;
}

package、import 关键字

package(包)

package,称为包,用于指明该文件中定义的类、接口等结构所在的包。

语法格式:

package 顶层包名.子包名;

举例:

在 pack1/pack2/PackageTest.java 文件中:

java
package pack1.pack2; // 指定类 PackageTest 属于包 pack1.pack2
public class PackageTest{
    public void display(){
        System.out.println("in  method display()");
    }
}

说明:

  • 一个源文件只能有一个声明包的 package 语句
  • package 语句作为 Java 源文件的第一条语句出现。若缺省该语句,则为无名包
  • 包名,属于标识符,需要满足标识符命名的规则和规范。取包名时不要使用 "java.xx" 名字,会起冲突
  • 包对应于文件系统的目录,package 语句中用“.”来指明包(目录)的层次,每 . 一次就表示一层文件目录
  • 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)。不同的包下可以定义同名的结构(类、接口)

包的作用:

  • 包可以包含类和子包,划分项目层次,便于管理
  • 帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC 的设计模式
  • 解决类命名冲突的问题
  • 控制访问权限

举例: MVC 是一种软件构件模式,目的是为了降低程序开发中代码业务的耦合度。

MVC 设计模式将整个程序分为三个层次:视图模型 (Viewer) 层,控制器 (Controller) 层,与数据模型 (Model) 层。

这种将程序输入输出、数据处理,以及数据的展示分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性。

视图层 viewer:显示数据,为用户提供使用界面,与用户直接进行交互。
 > 相关工具类:view.utils
 > 自定义试图:view.ui
 
控制层 controller:解析用户请求,处理业务逻辑,给予用户响应
 > 应用界面相关:controller.activity
 > 存放 fragment:controller.fragment
 > 显示列表的适配器:controller.adapter
 > 服务相关:controller.service
 > 抽取的基类:controller.base
    
模型层 model:主要承载数据、处理数据
 > 数据对象封装:model.bean/domain
 > 数据库操作类:model.dao
 > 数据库:model.db

JDK 中主要的包介绍:

  • java.lang 包含一些 Java 语言的核心类,如 String、Math、Integer、System 和 Thread,提供常用功能
  • java.net 包含执行与网络相关的操作的类和接口
  • java.io 包含能提供多种输入/输出功能的类
  • java.util 包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关类的函数
  • java.text 包含了一些 java 格式化相关的类
  • java.sql 包含了 java 进行 JDBC 数据库编程的相关类/接口
  • java.awt 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面 (GUI)

import(导入)

为了使用定义在其它包中的 Java 类,需用 import 语句来显式引入指定包下所需要的类。相当于 import 语句告诉编译器到哪里去寻找这个类。

语法格式

import 包名.类名;

应用举例

java
import pack1.pack2.Test; // import pack1.pack2.*; 表示引入 pack1.pack2 包中的所有结构
public class PackTest{
    public static void main(String args[]){
        Test t = new Test(); // Test 类在 pack1.pack2 包中定义
        t.display();
    }
}

注意事项

  • import 语句,声明在包的声明和类的声明之间
  • 如果需要导入多个类或接口,那么就并列显式多个 import 语句即可
  • 如果使用 a.* 导入结构,表示可以导入 a 包下的所有的结构。举例:可以使用 java.util.* 的方式,一次性导入 util 包下所有的类或接口
  • 如果导入的类或接口是 java.lang 包下的,或者是当前包下的,则可以省略此 import 语句
  • 如果已经导入 java.a 包下的类,那么如果需要使用 a 包的子包下的类的话,仍然需要导入
  • 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类
  • import static 组合的作用:调用指定类或接口下的静态的属性或方法

面向对象特征一:封装性(encapsulation)

Java 权限修饰符范围

修饰符本类内部本包内其它包的子类其它包非子类
private×××
缺省××
protected×
public

封装性的体现

私有化类的成员变量,提供公共的 get 和 set 方法,对外暴露获取和修改属性的功能。

提供 getXxx/setXxx 方法,可以访问成员变量,代码如下:

java
public class Person {
    private String name;
    private int age;
    private boolean marry;
    public void setName(String n) {
        name = n;
    }
    public String getName() {
        return name;
    }
    public void setAge(int a) {
        age = a;
    }
    public int getAge() {
        return age;
    }
    public void setMarry(boolean m){
        marry = m;
    }
    public boolean isMarry(){
        return marry;
    }
}

测试:

java
public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person();
        // 实例变量私有化,跨类是无法直接使用的
//        p.name = "张三";
//        p.age = 23;
//        p.marry = true;
//        p.setName("张三");
        System.out.println("p.name = " + p.getName());
        p.setAge(23);
        System.out.println("p.age = " + p.getAge());
        p.setMarry(true);
        System.out.println("p.marry = " + p.isMarry());
    }
}

成员变量封装的好处:

  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。 还可以进行数据检查,从而有利于保证对象信息的完整性
  • 便于修改,提高代码的可维护性。在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。 例如:Java8 -> Java9,String 从 char[] 转为 byte[] 内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改

类的成员之三:构造器(Constructor)

我们 new 完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。 我们能不能在 new 对象时,直接为当前对象的某个或所有成员变量直接赋值呢? 可以,Java 给我们提供了构造器(Constructor) ,也称为构造方法。

构造器的作用

new 对象,并在 new 对象的时候为实例变量赋值。

构造器的语法格式

[修饰符] class 类名{
    [修饰符] 构造器名(){
        // 实例初始化代码
    }
    [修饰符] 构造器名(参数列表){
        // 实例初始化代码
    }
}

说明

  1. 构造器名必须与它所在的类名必须相同
  2. 它没有返回值,所以不需要返回值类型,也不需要 void
  3. 构造器的修饰符只能是权限修饰符,不能被其他任何修饰。比如,不能被 static、final、synchronized、abstract、native 修饰, 不能有 return 语句返回值

举例:

java
public class Student {
    private String name;
    private int age;
    // 无参构造
    public Student() {}
    // 有参构造
    public Student(String n,int a) {
        name = n;
        age = a;
    }
    public String getName() {
        return name;
    }
    public void setName(String n) {
        name = n;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int a) {
        age = a;
    }
    public String getInfo(){
        return "姓名:" + name +",年龄:" + age;
    }
}
java
public class TestStudent {
    public static void main(String[] args) {
        // 调用无参构造创建学生对象
        Student s1 = new Student();
        // 调用有参构造创建学生对象
        Student s2 = new Student("张三",23);
        System.out.println(s1.getInfo());
        System.out.println(s2.getInfo());
    }
}

使用说明

  1. 当我们没有显式的声明类中的构造器时,系统会默认提供一个无参的构造器并且该构造器的修饰符默认与类的修饰符相同
  2. 当我们显式的定义类的构造器以后,系统就不再提供默认的无参的构造器了
  3. 在类中,至少会存在一个构造器
  4. 构造器是可以重载的

阶段性知识补充

类中属性赋值

1、在类的属性中,可以有哪些位置给属性赋值?

  1. 默认初始化
  2. 显式初始化
  3. 构造器中初始化
  4. 通过"对象。属性"或"对象。方法"的方式,给属性赋值

2、这些位置执行的先后顺序是怎样?

顺序:① - ② - ③ - ④。

上述中的 ①、②、③ 在对象创建过程中,只执行一次。 ④ 是在对象创建后执行的,可以根据需求多次执行。

JavaBean

JavaBean 是一种 Java 语言写成的可重用组件。

所谓 JavaBean,是指符合如下标准的 Java 类:

  1. 类是公共的
  2. 有一个无参的公共的构造器
  3. 有属性,且有对应的 get、set 方法

示例:

java
public class JavaBean {
    private String name; // 属性一般定义为 private
    private int age;
    public JavaBean() {
    }
    public int getAge() {
        return age;
    }
    public void setAge(int a) {
        age = a;
    }
    public String getName() {
        return name;
    }
    public void setName(String n) {
        name = n;
    }
}

UML 类图

  • UML(Unified Modeling Language,统一建模语言),用来描述软件模型和架构的图形化语言
  • 常用的 UML 工具软件有 PowerDesinger、Rose 和 Enterprise Architect
  • UML 工具软件不仅可以绘制软件开发中所需的各种图表,还可以生成对应的源代码
  • 在软件开发中,使用 UML 类图可以更加直观地描述类内部结构(类的属性和操作)以及类之间的关系(如关联、依赖、聚合等)

Released under the MIT License.