0%

java编程学习之面向对象编程

前言

昨天我们快速入门了,今天继续学习基础知识,面向对象编程到底是怎么面向的?

面向对象基础

方法

一个class可以包含多个field,例如,我们给Person类就定义了两个field:
class Person {
public String name;
public int age;
}
直接把field用public暴露给外部可能会破坏封装性。为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问。
简单讲,就是函数体内的变量,不能由外部直接访问,这样太危险了,得有个中间人。于是就通过方法连接privatepublic

  • 举例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class PrivatePublic {
    public static void main(String[] args) {
    Person ming = new Person();
    ming.setName("Xiao Ming"); // 设置name
    ming.setAge(120); // 设置age
    System.out.println(ming.getName() + ", " + ming.getAge());
    }
    }

    class Person {
    private String name; //private变量
    private int age; //private变量

    public String getName() { //使用这个方法,返回private变量的值,相当于把private变量public化
    return this.name; //this指的就是当前实例:Person
    }

    public void setName(String name) { //使用这个方法,设置name的值,相当于赋值的过程
    this.name = name;
    }

    public int getAge() {
    return this.age;
    }

    public void setAge(int age) {
    if (age < 0 || age > 100) { //如果传入的参数age不符合条件,则程序抛出异常
    throw new IllegalArgumentException("invalid age value"); //这是抛出异常的提示信息
    }
    this.age = age;
    }
    }
    image.png
  • 可变参数
    可变参数,意思就是这个参数的值不确定。比如传入的参数是数组,不确定数组大小时,就要用可变参数。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Group {
    private String[] names;

    public void setNames(String... names) { //String...指的就是传入参数是一个大小不确定的数组
    this.names = names;
    }
    }
    //调用方法
    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
    等价于:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Group {
    private String[] names;

    public void setNames(String[] names) { //String...和String[]这两种写法都对
    this.names = names;
    }
    }
    //调用方法:调用方需要自己先构造String[]
    Group g = new Group();
    g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
    String...String[]的区别:
  1. String...无法传入null。
  2. String[]可以传入null。调用方需要自己先构造String[]。
    1
    2
    Group g = new Group();
    g.setNames(null);
  • 参数绑定
    调用方把参数传递给实例方法时,调用时传递的值会按参数位置一一绑定。
    举例:基本数据类型的参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ParamBundle {
    public static void main(String[] args) {
    Persons p = new Persons();
    int n = 15; // n的值为15
    p.setAge(n); // 传入n的值
    System.out.println(p.getAge()); // 15
    n = 20; // n的值改为20
    System.out.println(p.getAge()); // 15,这里虽然n的值改变了,但由于setAge方法没有被调用,所以age的值仍然不变
    }
    }

    class Persons {
    private int age;

    public int getAge() {
    return this.age;
    }

    public void setAge(int age) {
    this.age = age;
    }
    }
    举例:引用类型的参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ParamTrans {
    public static void main(String[] args) {
    People p = new People();
    String[] fullname = new String[] { "Homer", "Simpson" }; //这里使用String[]类型参数时,调用方需要自己先构造String[]
    p.setName(fullname); // 传入fullname数组
    System.out.println(p.getName()); // "Homer Simpson"
    fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
    System.out.println(p.getName()); // "Bart Simpson",getName方法的返回值是由name[0]和name[1]共同决定的,name[0]改变了,getName()的返回值也就变了
    }
    }

    class People {
    private String[] name;

    public String getName() {
    return this.name[0] + " " + this.name[1];
    }

    public void setName(String[] name) {
    this.name = name;
    }
    }
    举例:引用类型参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ParamBundleQuote {
    public static void main(String[] args) {
    Per p = new Per();
    String bob = "Bob";
    p.setName(bob); // 传入bob变量
    System.out.println(p.getName()); // "Bob"
    bob = "Alice"; // bob改名为Alice
    System.out.println(p.getName()); // "Bob",getName方法不受传递参数的影响,直接返回类的属性值name
    }
    }

    class Per {
    private String name;

    public String getName() {
    return this.name;
    }

    public void setName(String name) {
    this.name = name;
    }
    }
    练习:模仿学习,新增setAge和getAge这两个方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class PracticeGetAge {
    public static void main(String[] args) {
    Please ming = new Please();
    ming.setName("小明");
    ming.setAge(12);
    System.out.println(ming.getAge());
    }
    }

    class Please{
    private String name;
    private int age;

    public int getAge() { //int表示返回值类型是整型
    return age;
    }

    public String getName() { //String表示返回值是引用类型字符串
    return name;
    }

    public void setAge(int age) { //void表示没有返回值,int age表示传入的参数age是整型
    this.age = age; //给当前类的属性age赋值,也就是给private变量赋值
    }

    public void setName(String name) { //void表示没有返回值,String name表示传入的参数name是引用类型字符串
    this.name = name;
    }
    }

    构造方法

    创建实例的时候,实际上是通过构造方法来初始化实例的。前面一节我们初始化两个变量一共是设置了setAge和setName这两个方法,如果要用100个变量呢,难道手动设置100个方法???天哪,想到这就头大。程序这么自动化的东西,怎么可能做这种傻事呢🙃🙃🙃
    于是,我们就想这样来偷懒,只创建一个方法,这个方法传递多个变量,同时返回多个变量的值。perfect,一个方法搞定所有变量的初始化。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class MakeMethod {
    public static void main(String[] args) {
    Pa p = new Pa("Xiao Ming", 15); //调用构造方法,必须使用new
    System.out.println(p.getName());
    System.out.println(p.getAge());
    }
    }

    class Pa {
    private String name;
    private int age;

    public Pa(String name, int age) { //这就是构造方法。没有返回值(void也没有),并且方法名和类名一致。
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return this.name;
    }

    public int getAge() {
    return this.age;
    }
    }
  • 默认构造方法
    如果我们没有自定义构造方法,编译器会帮助我们创建一个默认的构造方法,像这样:
    1
    2
    3
    4
    class Person {
    public Person() {
    }
    }
    所以我们没有定义构造方法时,也可以成功使用new调用构造方法。当然如果我们已经自定义了构造方法,编译器就不会画蛇添足了。
  • 多构造方法
    一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
    举例:这个调用关系就是Pi()——>Pi(String name)——>Pi(String name, int age),所以我们最终得到的结果是”unnamed,18”。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class MultiMethod {
    public static void main(String[] args) {
    Pi p = new Pi();
    System.out.println(p.getName());
    System.out.println(p.getAge());
    }
    }

    class Pi {
    private String name;
    private int age;

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

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

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

    public String getName() {
    return this.name;
    }
    public int getAge() {
    return this.age;
    }
    }

    方法重载

    方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
    注意:方法重载的返回值类型通常都是相同的。
    这种方法名相同,但各自的参数不同,称为方法重载(Overload)。
    举例:
    String类提供了多个重载方法indexOf(),可以查找子串:
  • int indexOf(int ch):根据字符的Unicode码查找;
  • int indexOf(String str):根据字符串查找;
  • int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置;
  • int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class ReloadMethod {
    public static void main(String[] args) {
    String s = "Test string";
    int n1 = s.indexOf('t');
    int n2 = s.indexOf("st");
    int n3 = s.indexOf("st", 4);
    System.out.println(n1);
    System.out.println(n2);
    System.out.println(n3);
    }
    }
    练习:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class PracticeReloadMethod {
    public static void main(String[] args) {
    Pson ming = new Pson();
    Pson hong = new Pson();
    ming.setName("Xiao Ming");
    // TODO: 给Pson增加重载方法setName(String, String):
    hong.setName("Xiao", "Hong");
    System.out.println(ming.getName());
    System.out.println(hong.getName());
    }
    }

    class Pson {
    private String name;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public void setName(String name1,String name2) {
    this.name = name1+" "+name2;
    }
    }
    运行结果:
    image.png

    继承

    想象有一个类是Person,有name和age这两个Field。我们需要定义另一个类Student,要包含name、age、score这三个Field。
    按照之前学过的定义类的方法,Student类的定义要重复再写一遍name和age,显然这样重复的工作和程序的简洁原则相违背。我们是不是可以在Person类的基础上扩充Field呢?没错,这就是继承了。打个比方,妈妈说家里需要采购水果、蔬菜、零食,第一天爸爸出门买了水果和蔬菜,第二天小明去采购的时候,是不是继承爸爸买的水果和蔬菜,只需要再买点零食就行了?差不多是这个意思。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class Inherit {
    public static void main(String[] args) {
    Stu s = new Stu(); //将变量s实例化
    s.setName("Dana"); //由于Stu继承了Pn类,所以可以使用Pn的属性.setName
    s.setAge(23);
    s.setScore(100);
    System.out.println(s.getName());
    System.out.println(s.getAge());
    System.out.println(s.getScore());
    }
    }

    class Pn {
    private String name;
    private int age;

    public String getName() {
    return this.name;
    }
    public void setName(String name) {
    this.name = name;
    }
    public int getAge() {
    return this.age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    }

    class Stu extends Pn { //Stu继承了Pn类
    // 不要重复name和age字段/方法,
    // 只需要定义新增score字段/方法:
    private int score;

    public int getScore() {
    return this.score;
    }
    public void setScore(int score) {
    this.score = score;
    }
    }
    运行结果:
    image.png
  • 基础知识:
    Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。(Object就是孙悟空,咱们普通人就是普通的类,有且仅有一个父亲~)
  • protected
    继承有个特点,就是子类无法访问父类的private字段或者private方法。
    为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问。
    很好理解,父亲不可能让子女继承一切的东西,他也有自己的隐私。他可以选择性地把一些财产拿给子女继承,这些财产的状态就是protected,只有子女后代可以继承,外人继承不了。
    举例:子类继承父类的protected字段时,可以直接使用,不必将private转换为public。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class InheritProtected {
    public static void main(String[] args) {
    St s = new St();
    s.name = "Dana";
    s.age = 23;
    System.out.println(s.hello());
    System.out.println(s.ageis());
    }
    }

    class Ps {
    protected String name;
    protected int age;
    }

    class St extends Ps {
    public String hello() {
    return "Hello, " + name; // OK!
    }

    public int ageis() {
    return this.age;
    }
    }
  • super
    引用父类的字段,有这几种方法:
  1. this.name
  2. name
  3. super.name
    在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
    如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。
    子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class InheritSuper {
    public static void main(String[] args) {
    Stud s = new Stud("Xiao Ming", 12, 89);
    System.out.println(s.getName());
    System.out.println(s.getAge());
    System.out.println(s.getScore());
    }
    }

    class Pers {
    protected String name;
    protected int age;

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

    class Stud extends Pers {
    protected int score;

    public Stud(String name, int age, int score) {
    super(name,age); //子类的构造方法里,第一句必须是调用父类的构造方法
    this.score = score;
    }

    public int getAge() {
    return age;
    }

    public String getName() {
    return name;
    }

    public int getScore() {
    return score;
    }
    }
  • 向上转型
    把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。因为子类继承父类,所以子类具备父类的全部Field,向上转型so easy。
    举例:
    1
    2
    3
    4
    Student s = new Student(); //假定Student是Person的子类
    Person p = s; // upcasting, ok
    Object o1 = p; // upcasting, ok
    Object o2 = s; // upcasting, ok
    继承树是Student > Person > Object,所以,可以把Student类型转型为Person,或者更高层次的Object。
    很好理解,子女继承了父亲的财产,阶层升level的可能性当然是很大的。或者从伦理角度分析,子女也会有子女,所以子类升级为父类,这样的向上转型很简单。
  • 向下转型
    把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。
    举例:
    1
    2
    3
    4
    Person p1 = new Student(); // upcasting, ok 这里相当于是把父亲定义为儿子:父亲是爷爷的儿子
    Person p2 = new Person(); //这里把父亲定义为父亲:父亲是小明的父亲
    Student s1 = (Student) p1; // ok 这句话就是说:因为父亲是爷爷的儿子,所以父亲是一个儿子
    Student s2 = (Student) p2; // runtime error! ClassCastException! 这句话是说:父亲是小明的父亲,父亲是一个儿子?!当然不对咯
    引申学习:instanceof的用法
    为了避免向下转型出错,Java提供了instanceof操作符。
    instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Person p = new Person(); //p是父亲
    System.out.println(p instanceof Person); // true p是不是父亲
    System.out.println(p instanceof Student); // false p是不是儿子

    Student s = new Student(); //s是儿子
    System.out.println(s instanceof Person); // true s是不是父亲的儿子
    System.out.println(s instanceof Student); // true s是不是儿子

    Student n = null; //n什么也不是
    System.out.println(n instanceof Student); // false n是不是儿子
  • 练习:新增子类PrimaryStudent,继承自父类Studen。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    public class NewSon {
    public static void main(String[] args) {
    Perso p = new Perso("小明", 12);
    Studen s = new Studen("小红", 20, 99);
    // TODO: 定义PrimaryStudent,从Studen继承,新增grade字段:
    PrimaryStudent ps = new PrimaryStudent("小军", 9, 100, 5);
    System.out.println(ps.getName());
    System.out.println(ps.getAge());
    System.out.println(ps.getScore());
    System.out.println(ps.getGrade());
    }
    }

    class Perso {
    protected String name; //protected可由子类继承
    protected int age;

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

    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }
    public void setAge(int age) {
    this.age = age;
    }
    }

    class Studen extends Perso {
    protected int score;

    public Studen(String name, int age, int score) {
    super(name, age); //调用父类Perso的构造方法
    this.score = score;
    }

    public int getScore() {
    return score;
    }
    }

    class PrimaryStudent extends Studen {
    protected int grade;

    public PrimaryStudent(String name, int age, int score, int grade) {
    super(name, age,score); //调用父类Studen的构造方法
    this.grade = grade;
    }

    public int getGrade() {
    return grade;
    }
    }

    多态

  • 复写
    在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
    完全相同指的是:参数和返回值都完全相同。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Override {
    public static void main(String[] args) {
    Psons p = new Studt();
    p.run(); // 打印结果是Studt.run 因为p指向的是Studt实例
    }
    }

    class Psons {
    public void run() {
    System.out.println("Pson.run");
    }
    }

    class Studt extends Psons {
    public void run() { //子类复写了父类的方法
    System.out.println("Studt.run");
    }
    }
  • 多态
    多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    public class PracticeOverride {
    public static void main(String[] args) {
    // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
    Income[] incomes = new Income[] { //将普通收入、工资收入和享受国务院特殊津贴这三类收入初始化为一个数组,数组元素初始化调用了相应的构造方法
    new Income(3000),
    new Salary(7500),
    new StateCouncilSpecialAllowance(15000)
    };
    System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) { //计算总纳税额的方法
    double total = 0;
    for (Income income: incomes) { //用for each对数组做遍历
    total = total + income.getTax(); //调用计算纳税额的方法
    }
    return total;
    }
    }

    class Income {
    protected double income;

    public Income(double income) { //Income的构造方法
    this.income = income;
    }

    public double getTax() { //计算纳税额的方法
    return income * 0.1; // 税率10%
    }
    }

    class Salary extends Income {
    public Salary(double income) { //Salary的构造方法
    super(income); //Salary构造方法的第一句必须调用父类Income的构造方法
    }

    public double getTax() { //计算纳税额的方法
    if (income <= 5000) {
    return 0;
    }
    return (income - 5000) * 0.2;
    }
    }

    class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) { //StateCouncilSpecialAllowance的构造方法
    super(income); //StateCouncilSpecialAllowance构造方法的第一句必须调用父类Income的构造方法
    }

    public double getTax() { //计算纳税额的方法
    return 0;
    }
    }
    运行结果:
    image.png
    脑子不好使了,明天再来……
    利用多态,totalTax()方法只需要和Income打交道,它完全不需要知道Salary和StateCouncilSpecialAllowance的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。
    比如我们新增稿费收入article。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66

    public class PracticeOverride {
    public static void main(String[] args) {
    // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
    Income[] incomes = new Income[] {
    new Income(3000),
    new Salary(7500),
    new StateCouncilSpecialAllowance(15000),
    new article(2000)
    };
    System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) {
    double total = 0;
    for (Income income: incomes) {
    total = total + income.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);
    }

    public double getTax() {
    if (income <= 5000) {
    return 0;
    }
    return (income - 5000) * 0.2;
    }
    }

    class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
    super(income);
    }

    public double getTax() {
    return 0;
    }
    }

    class article extends Income {
    public article(double income) {
    super(income);
    }

    public double getTax() {
    return income * 0.2;
    }
    }
  • 复写Object方法
    所有的class最终都继承自Object,而Object定义了以下重要的方法:
    toString():把instance输出为String;
    equals():判断两个instance是否逻辑相等;
    hashCode():计算一个instance的哈希值。
  • 调用super
    在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
  • final
    如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override。

    抽象类

  • 抽象类
    如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。
    我们无法实例化一个抽象类。
    抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Abstract {
    public static void main(String[] args) {
    Pe p = new Studs();
    p.run();
    }
    }

    abstract class Pe {
    public abstract void run(); //相当于定义一个规范,要求子类必须调用run()方法
    }

    class Studs extends Pe {
    public void run() { //子类Studs必须调用父类的run()方法
    System.out.println("Student.run");
    }
    }
    运行结果:
    image.png
  • 面向抽象编程
    这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
    面向抽象编程的本质就是:
  1. 上层代码只定义规范(例如:abstract class Person);
  2. 不需要子类就可以实现业务逻辑(正常编译);
  3. 具体的业务逻辑由不同的子类实现,调用者并不关心。
    举例:
    当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例:
    1
    2
    Person s = new Student();
    Person t = new Teacher();
    这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:
    // 不关心Person变量的具体子类型:
    1
    2
    s.run();
    t.run();

    接口

    所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
    1
    2
    3
    4
    interface Person { //定义接口
    void run();
    String getName();
    }
    当一个具体的class去实现一个interface时,需要使用implements关键字。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Student implements Person { //Student类实现接口Person
    private String name;

    public Student(String name) {
    this.name = name;
    }

    @Override //实现接口Person的run()方法
    public void run() {
    System.out.println(this.name + " run");
    }

    @Override //实现接口Person的getName()方法
    public String getName() {
    return this.name;
    }
    }
    一个类可以实现多个interface。
    1
    2
    3
    class Student implements Person, Hello { // 实现了两个interface
    ...
    }
  • 抽象类和接口的对比:
    image.png
  • 接口继承
    interface继承自interface使用extends,它相当于扩展了接口的方法。
    1
    2
    3
    4
    5
    6
    7
    8
    interface Hello {
    void hello();
    }

    interface Person extends Hello { //Person接口继承了Hello接口
    void run();
    String getName();
    }
  • default方法
    default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class Default {
    public static void main(String[] args) {
    P p = new S("Xiao Ming");
    P p2 = new W("dana"); //类W继承自接口P,它可以调用接口P新增的default方法run2()
    p.run();
    p2.run2();
    }
    }

    interface P {
    String getName();
    default void run() {
    System.out.println(getName() + " run");
    }

    default void run2() { //接口P新增default方法run2()
    System.out.println(getName() + " run2");
    }
    }

    class S implements P {
    private String name;

    public S(String name) {
    this.name = name;
    }

    public String getName() {
    return this.name;
    }
    }

    class W implements P { //类W继承自接口P
    private String name;

    public W(String name) {
    this.name = name;
    }

    public String getName() {
    return this.name;
    }
    }
    运行结果:
    image.png

    静态字段和静态方法

    在一个class中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。
    用static修饰的字段,称为静态字段:static field。
    实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。
  • 静态字段
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class StaticField {
    public static void main(String[] args) {
    Pns ming = new Pns("Xiao Ming", 12);
    Pns hong = new Pns("Xiao Hong", 15);
    ming.number = 88; //使用静态字段number
    System.out.println(hong.number);
    hong.number = 99;
    System.out.println(ming.number);
    }
    }

    class Pns {
    public String name;
    public int age;

    public static int number; //静态字段

    public Pns(String name, int age) {
    this.name = name;
    this.age = age;
    }
    }
    运行结果:
    image.png

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

推荐写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StaticField {
public static void main(String[] args) {
Pns ming = new Pns("Xiao Ming", 12);
Pns hong = new Pns("Xiao Hong", 15);
Pns.number = 99;
System.out.println(hong.number);
System.out.println(ming.number);
}
}

class Pns {
public String name;
public int age;

public static int number;

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

运行结果:
image.png

  • 静态方法
    用static修饰的方法称为静态方法。
    Java程序的入口main()也是静态方法。
    调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class StaticMethod {
    public static void main(String[] args) {
    Paa.setNumber(99); //通过类名直接调用静态方法setNumber(int value)
    System.out.println(Paa.number);
    }
    }

    class Paa {
    public static int number;

    public static void setNumber(int value) { //静态方法setNumber(int value)
    number = value;
    }
    }
  • 接口的静态字段
    因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型。
    因为interface的字段只能是public static final类型,所以我们可以把修饰符都去掉。
    1
    2
    3
    4
    5
    public interface Person {
    // 编译器会自动加上public static final:
    int MALE = 1;
    int FEMALE = 2;
    }

    Java编译器最终编译出的.class文件只使用完整类名,因此,在代码中,当编译器遇到一个class名称时:
  1. 如果是完整类名,就直接根据完整类名查找这个class;
  2. 如果是简单类名,按下面的顺序依次查找:
    查找当前package是否存在这个class;
    查找import的包是否包含这个class;
    查找java.lang包是否包含这个class。
  3. 如果按照上面的规则还无法确定类名,则编译报错。
    举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import java.text.Format;

    public class Main {
    public static void main(String[] args) {
    java.util.List list; // ok,使用完整类名 -> java.util.List
    Format format = null; // ok,使用import的类 -> java.text.Format
    String s = "hi"; // ok,使用java.lang包的String -> java.lang.String
    System.out.println(s); // ok,使用java.lang包的System -> java.lang.System
    MessageFormat mf = null; // 编译错误:无法找到MessageFormat: MessageFormat cannot be resolved to a type
    }
    }
    运行结果:
    image.png

    作用域

    Java内建的访问权限包括public、protected、private和package权限;
    Java在方法内部定义的变量是局部变量,局部变量的作用域从变量声明开始,到一个块结束;
    final修饰符不是访问权限,它可以修饰class、field和method;
    一个.java文件只能包含一个public类,但可以包含多个非public类。
  • public
    定义为public的class、interface可以被其他任何类访问。
    定义为public的field、method可以被其他类访问,前提是首先有访问class的权限。
  • private
    定义为private的field、method无法被其他类访问。
    private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法。
  • protected
    protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类。
  • package
    包作用域是指一个类允许访问同一个package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法。
    只要在同一个包,就可以访问package权限的class、field和method。
    包没有父子关系,com.apache和com.apache.abc是不同的包。
  • 局部变量
    在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。
  • final
    用final修饰class可以阻止被继承。
    用final修饰method可以阻止被子类覆写。
    用final修饰field可以阻止被重新赋值。
    用final修饰局部变量可以阻止被重新赋值。

    classpath和jar

  • classpath
    classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class。
    classpath的设定方法有两种:
  1. 在系统环境变量中设置classpath环境变量,不推荐;
  2. 在启动JVM时设置classpath变量,推荐。
    实际上就是给java命令传入-classpath或-cp参数。
    java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
  • jar包
    把package组织的目录层级,以及各个目录下的所有文件(包括.class文件和其他文件)都打成一个jar文件,方便下载和使用。
    jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class,就可以把jar包放到classpath中。
    jar包如何创建?
    因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip改为.jar,一个jar包就创建成功。
    Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。

    模块

    所以,jar只是用于存放class的容器,它并不关心class之间的依赖。
    从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种自带“依赖关系”的class容器就是模块
  • 编写模块
  • 运行模块
  • 打包JRE
  • 访问权限
    模块进一步隔离了代码的访问权限。
    具体操作明天继续……这个部分得好好消化,信息量略大……
-------------本文结束感谢您的阅读-------------