面向对象基础

  • 理解:

    • class是一种对象模版,定义了如何创建实例,class本身就是一种数据类型

    • instance是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同

    • 定义class

      • 创建一个类,例如,给这个类命名为Person,就是定义一个class

        class Person {
            public String name;
            public int age;
        }
        
      • class Book {
            public String name;
            public String author;
            public String isbn;
            public double price;
        }
        
    • 创建实例

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

        Person ming = new Person();
        

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

      • ming.name = "Xiao Ming"; // 对字段name赋值
        ming.age = 12; // 对字段age赋值
        System.out.println(ming.name); // 访问字段name
        
        Person hong = new Person();
        hong.name = "Xiao Hong";
        hong.age = 15;
        

        上述两个变量分别指向两个不同的实例,它们在内存中的结构如下:

                    ┌──────────────────┐
        ming ──────▶│Person instance   │
                    ├──────────────────┤
                    │name = "Xiao Ming"│
                    │age = 12          │
                    └──────────────────┘
                    ┌──────────────────┐
        hong ──────▶│Person instance   │
                    ├──────────────────┤
                    │name = "Xiao Hong"│
                    │age = 15          │
                    └──────────────────┘
        
  • 方法

    • 意义:

      • 一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性

        外部代码不能直接读取private字段,但可以通过getName()getAge()间接获取private字段的值

      • **调用方法**的语法是实例变量.方法名(参数);。一个方法调用就是一个语句,所以不要忘了在末尾加;

        ming.setName("Xiao Ming");
        
    • 定义方法

      修饰符 方法返回类型 方法名(方法参数列表) {
          若干方法语句;
          return 方法返回值;
      }
      

      方法返回值通过return语句实现,如果没有返回值,返回类型设置为void,可以省略return

    • 定义private方法的理由是内部方法是可以调用private方法的

    • this变量

      • 在方法内部,可以使用一个隐含的变量this,它始终指向当前实例

      • 如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this

        class Person {
            private String name;
        
            public void setName(String name) {
                this.name = name; // 前面的this不可少,少了就变成局部变量name了
            }
        }
        
    • 方法参数

      • 包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。

        class Person {
            ...
            public void setNameAndAge(String name, int age) {
                ...
            }
        }
        
    • 可变参数

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

        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
        
    • 参数绑定

      • 基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响

        // 基本类型参数绑定
        public class Main {
            public static void main(String[] args) {
                Person p = new Person();
                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
            }
        }
        
        class Person {
            private int age;
        
            public int getAge() {
                return this.age;
            }
        
            public void setAge(int age) {
                this.age = age;
            }
        }
        
      • 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)

        // 引用类型参数绑定
        public class Main {
            public static void main(String[] args) {
                Person p = new Person();
                String[] fullname = new String[] { "Homer", "Simpson" };
                p.setName(fullname); // 传入fullname数组
                System.out.println(p.getName()); // "Homer Simpson"
                fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
                System.out.println(p.getName()); // "Bart Simpson"
            }
        }
        
        class Person {
            private String[] name;
        
            public String getName() {
                return this.name[0] + " " + this.name[1];
            }
        
            public void setName(String[] name) {
                this.name = name;
            }
        }
        
      • string

        public class Main {
            public static void main(String[] args) {
                Person p = new Person();
                String bob = "Bob";
                p.setName(bob); // 传入bob变量
                System.out.println(p.getName()); // "Bob"
                bob = "Alice"; // bob改名为Alice
                System.out.println(p.getName()); // "Bob"
            }
        }
        
        class Person {
            private String name;
        
            public String getName() {
                return this.name;
            }
        
            public void setName(String name) {
                this.name = name;
            }
        }
        
  • 构造方法

    • 创建对象实例时就把内部字段全部初始化为合适的值

      • // 构造方法
        public class Main {
            public static void main(String[] args) {
                Person p = new Person("Xiao Ming", 15);
                System.out.println(p.getName());
                System.out.println(p.getAge());
            }
        }
        
        class Person {
            private String name;
            private int age;
        
            public Person(String name, int age) {
                this.name = name;
                this.age = age;
            }
        
            public String getName() {
                return this.name;
            }
        
            public int getAge() {
                return this.age;
            }
        }
        
    • 默认构造方法

      • 特别注意的是,如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法:

        // 构造方法
        public class Main {
            public static void main(String[] args) {
                Person p = new Person(); // 编译错误:找不到这个构造方法
            }
        }
        
        class Person {
            private String name;
            private int age;
        
            public Person(String name, int age) {
                this.name = name;
                this.age = age;
            }
        
            public String getName() {
                return this.name;
            }
        
            public int getAge() {
                return this.age;
            }
        }
        
      • 既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来:

        // 构造方法
        public class Main {
            public static void main(String[] args) {
                Person p1 = new Person("Xiao Ming", 15); // 既可以调用带参数的构造方法
                Person p2 = new Person(); // 也可以调用无参数构造方法
            }
        }
        
        class Person {
            private String name;
            private int age;
        
            public Person() {
            }
        
            public Person(String name, int age) {
                this.name = name;
                this.age = age;
            }
        
            public String getName() {
                return this.name;
            }
        
            public int getAge() {
                return this.age;
            }
        }
        
      • 没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false

        class Person {
            private String name; // 默认初始化为null
            private int age; // 默认初始化为0
        
            public Person() {
            }
        }
        
    • 多个构造方法

      • 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 = name;
                this.age = 12;
            }
        
            public Person() {
            }
        }
        

        如果调用new Person("Xiao Ming", 20);,会自动匹配到构造方法public Person(String, int)

        如果调用new Person("Xiao Ming");,会自动匹配到构造方法public Person(String)

        如果调用new Person();,会自动匹配到构造方法public Person()

      • 可以改为

        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)
            }
        }
        
  • 方法重载

    • 有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法

    • class Hello {
          public void hello() {
              System.out.println("Hello, world!");
          }
      
          public void hello(String name) {
              System.out.println("Hello, " + name + "!");
          }
      
          public void hello(String name, int age) {
              if (age < 18) {
                  System.out.println("Hi, " + name + "!");
              } else {
                  System.out.println("Hello, " + name + "!");
              }
          }
      }
      

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

      重载目的:功能类似的方法使用同一名字,更容易记住,调用起来更简单

  • 继承

    • 可以复用代码

      class Person {
          private String name;
          private int age;
      
          public String getName() {...}
          public void setName(String name) {...}
          public int getAge() {...}
          public void setAge(int age) {...}
      }
      
      class Student extends Person {
          // 不要重复name和age字段/方法,
          // 只需要定义新增score字段/方法:
          private int score;
      
          public int getScore() {  }
          public void setScore(int score) {  }
      }
      

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

    • prtected

      子类无法访问父类的private字段或者private方法。

      例如,Student类就无法访问Person类的nameage字段:

      class Person {
          private String name;
          private int age;
      }
      
      class Student extends Person {
          public String hello() {
              return "Hello, " + name; // 编译错误:无法访问name字段
          }
      }
      

      让子类可以访问父类的字段,我们需要把private改为protected

      protected修饰的字段可以被子类访问:

      class Person {
          protected String name;
          protected int age;
      }
      
      class Student extends Person {
          public String hello() {
              return "Hello, " + name; // OK!
          }
      }
      
    • super

      super关键字表示父类(超类)。子类引用父类的字段时,可用super.fieldName

      public class Main {
          public static void main(String[] args) {
              Student s = new Student("Xiao Ming", 12, 89);
          }
      }
      
      class Person {
          protected String name;
          protected int age;
      
          public Person(String name, int age) {
              this.name = name;
              this.age = age;
          }
      }
      
      class Student extends Person {
          protected int score;
      
          public Student(String name, int age, int score) {
              this.score = score;
          }
      }
      

      以上运行就会失误

      解决方法是调用Person类存在的某个构造方法。例如:

      class Student extends Person {
          protected int score;
      
          public Student(String name, int age, int score) {
              super(name, age); // 调用父类的构造方法Person(String, int)
              this.score = score;
          }
      }
      
    • 阻止继承

      • 只要某个class没有final修饰符,那么任何类都可以从该class继承

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

        例如,定义一个Shape类:

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

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

        public final class Rect extends Shape {...}
        

        是没问题的,因为Rect出现在Shapepermits列表中。但是,如果定义一个Ellipse就会报错:

        public final class Ellipse extends Shape {...}
        // Compile error: class is not allowed to extend sealed class: Shape
        
    • 向上转型

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

        向上转型实际上是把一个子类型安全地变为更加抽象的父类型:

        Student s = new Student();
        Person p = s; // upcasting, ok
        Object o1 = p; // upcasting, ok
        Object o2 = s; // upcasting, ok
        
    • 向下转型

      • 把一个父类类型强制转型为子类类型,就是向下转型

        Person p1 = new Student(); // upcasting, ok
        Person p2 = new Person();
        Student s1 = (Student) p1; // ok
        Student s2 = (Student) p2; // runtime error! ClassCastException!
        
      • instanceof判断一个变量所指向的实例是否指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false

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

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

    • 继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为重写

      方法名相同,方法参数相同,但方法返回值不同,也是不同的方法

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

    • 多态的特性就是,运行期才能动态决定调用的子类方法

      public class Main {
          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) {
                  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);
          }
      
          @Override
          public double getTax() {
              if (income <= 5000) {
                  return 0;
              }
              return (income - 5000) * 0.2;
          }
      }
      
      class StateCouncilSpecialAllowance extends Income {
          public StateCouncilSpecialAllowance(double income) {
              super(income);
          }
      
          @Override
          public double getTax() {
              return 0;
          }
      }
      
    • 重写Object方法

      因为所有的class最终都继承自Object,而Object定义了几个重要的方法:

      • toString():把instance输出为String
      • equals():判断两个instance是否逻辑相等
      • hashCode():计算一个instance的哈希值
      class Person {
          ...
          // 显示更有意义的字符串:
          @Override
          public String toString() {
              return "Person:name=" + name;
          }
      
          // 比较是否相等:
          @Override
          public boolean equals(Object o) {
              // 当且仅当o为Person类型:
              if (o instanceof Person) {
                  Person p = (Person) o;
                  // 并且name字段相同时,返回true:
                  return this.name.equals(p.name);
              }
              return false;
          }
      
          // 计算hash:
          @Override
          public int hashCode() {
              return this.name.hashCode();
          }
      }
      
    • 调用super

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

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

      • final修饰的方法不能被Override

        class Person {
            protected String name;
            public final String hello() {
                return "Hello, " + name;
            }
        }
        
        class Student extends Person {
            // compile error: 不允许覆写
            @Override
            public String hello() {
            }
        }
        
      • final修饰的类不能被继承

        final class Person {
            protected String name;
        }
        
        // compile error: 不允许继承自Person
        class Student extends Person {
        }
        
      • final修饰的字段在初始化后不能被修改

        class Person {
            public final String name = "Unamed";
        }
        
        Person p = new Person();
        p.name = "New Name"; // compile error!
        

        可在构造方法中初始化final字段:

        class Person {
            public final String name;
            public Person(String name) {
                this.name = name;
            }
        }
        
  • 抽象类

    • 如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,可以把父类的方法声明为抽象方法

      class Person {
          public abstract void run();//无法编译`Person`类
      }
      

      方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。Person类也无法被实例化

      必须把Person类本身也声明为abstract,才能正确编译它:

      abstract class Person {
          public abstract void run();
      }
      
    • 使用abstract修饰的类就是抽象类。我们无法实例化一个抽象类:

      Person p = new Person(); // 编译错误
      

      抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错

    • 抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力

  • 接口

    • 一个抽象类没有字段,所有方法全部都是抽象方法

      abstract class Person {
          public abstract void run();
          public abstract String getName();
      }
      

      可以把该抽象类改写为接口:interface

      interface Person {
          void run();
          String getName();
      }
      

      接口定义的所有方法默认都是public abstract的,这两个修饰符不需要写出来

    • 当一个具体的class去实现一个interface时,使用implements关键字

      class Student implements Person {
          private String name;
      
          public Student(String name) {
              this.name = name;
          }
      
          @Override
          public void run() {
              System.out.println(this.name + " run");
          }
      
          @Override
          public String getName() {
              return this.name;
          }
      }
      
    • 接口继承

      interface继承自interface使用extends,它相当于扩展了接口的方法

      interface Hello {
          void hello();
      }
      
      interface Person extends Hello {
          void run();
          String getName();
      }
      

      Person接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello接口

    • default方法

      // interface
      public class Main {
          public static void main(String[] args) {
              Person p = new Student("Xiao Ming");
              p.run();//Xiao Ming run
          }
      }
      
      interface Person {
          String getName();
          default void run() {
              System.out.println(getName() + " run");
          }
      }
      
      class Student implements Person {
          private String name;
      
          public Student(String name) {
              this.name = name;
          }
      
          public String getName() {
              return this.name;
          }
      }
      

      default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类; 如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法

  • 静态字段和静态方法

    • class中定义的字段,我们称之为实例字段,特点是,每个实例都有独立的字段,各个实例的同名字段互不影响

    • static修饰的字段,称为静态字段,只有一个共享“空间”,所有实例都会共享该字段

    • public class Main {
          public static void main(String[] args) {
              Person ming = new Person("Xiao Ming", 12);
              Person hong = new Person("Xiao Hong", 15);
              ming.number = 88;
              System.out.println(hong.number);
              hong.number = 99;
              System.out.println(ming.number);
          }
      }
      
      class Person {
          public String name;
          public int age;
      
          public static int number;
      
          public Person(String name, int age) {
              this.name = name;
              this.age = age;
          }
      }
      

      推荐用类名来访问静态字段。可以把静态字段理解为描述class本身的字段

      Person.number = 99;
      System.out.println(Person.number);
      
    • 静态方法:static修饰的方法称为静态方法

      调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用

      public class Main {
          public static void main(String[] args) {
              Person.setNumber(99);
              System.out.println(Person.number);
          }
      }
      
      class Person {
          public static int number;
      
          public static void setNumber(int value) {
              number = value;
          }
      }
      

      静态方法属于class而不属于实例,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段

    • 接口的静态字段

      interface字段只能public static final,可把这些修饰符都去掉,可简写为:

      public interface Person {
          // 编译器会自动加上public static final:
          int MALE = 1;
          int FEMALE = 2;
      }
      

      编译器会自动把该字段变为public static final类型

    • Java定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名

      小明的Person类存放在包ming下面,因此,完整类名是ming.Person

      JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays

    • 在定义class的时候,需要在第一行声明这个class属于哪个包。

      小明的Person.java文件:

      package ming; // 申明包名ming
      
      public class Person {
      }
      
    • 包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系

    • 包结构把上面的Java文件组织

      • 假设以package_sample作为根目录,src作为源码目录,那所有文件结构就是:

        package_sample
        └─ src
            ├─ hong
            │  └─ Person.java
            │  ming
            │  └─ Person.java
            └─ mr
               └─ jun
                  └─ Arrays.java
        

        即所有Java文件对应的目录层次要和包的层次一致

      • 编译后的.class文件也需要按照包结构存放。如果使用IDE,把编译后的.class文件放到bin目录下,那么,编译的文件结构就是:

        package_sample
        └─ bin
           ├─ hong
           │  └─ Person.class
           │  ming
           │  └─ Person.class
           └─ mr
              └─ jun
                 └─ Arrays.class
        
    • import

      • 一个class中,我们总会引用其他的class

      • 写法是用import语句,导入小军的Arrays,然后写简单类名:

        // Person.java
        package ming;
        
        // 导入完整类名:
        import mr.jun.Arrays;
        
        public class Person {
            public void run() {
                // 写简单类名: Arrays
                Arrays arrays = new Arrays();
            }
        }
        
      • import的时候,可以使用*,表示把这个包下面的所有class都导入进来

        import mr.jun.*;// 导入mr.jun包的所有class:
        

        一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包

  • 作用域:修饰符可用来限定访问作用域

    • public

      定义为publicclassinterface可以被其他任何类访问:

      package abc;
      
      public class Hello {
          public void hi() {
          }
      }
      

      上面的Hellopublic,因此,可以被其他包的类访问:

      package xyz;
      
      class Main {
          void foo() {
              // Main可以访问Hello
              Hello h = new Hello();
          }
      }
      
    • private

      定义为privatefieldmethod无法被其他类访问:

      package abc;
      
      public class Hello {
          // 不能被其他类调用:
          private void hi() {
          }
      
          public void hello() {
              this.hi();
          }
      }
      

      实际上,确切地说,private访问权限被限定在class的内部,而且与方法声明顺序无关推荐把private方法放到后面

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

        // 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();
                }
            }
        }
        
    • protected

      • protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类:

        package abc;
        
        public class Hello {
            // protected方法:
            protected void hi() {
            }
        }
        
        package xyz;
        
        class Main extends Hello {
            void foo() {
                // 可以访问protected方法:
                hi();
            }
        }
        
    • package

      • 包作用域是指一个类允许访问同一个package的没有publicprivate修饰的class,以及没有publicprotectedprivate修饰的字段和方法

        package abc;
        // package权限的类:
        class Hello {
            // package权限的方法:
            void hi() {
            }
        }
        
        package abc;
        
        class Main {
            void foo() {
                // 可以访问package权限的类:
                Hello h = new Hello();
                // 可以调用package权限的方法:
                h.hi();
            }
        }
        
    • 局部变量

      • 在方法内部定义的变量称为局部变量,作用域**从变量声明处开始到对应的块结束**
    • final

      • final修饰class可以阻止被继承:

        package abc;
        
        // 无法被继承:
        public final class Hello {
            private int n = 0;
            protected void hi(int t) {
                long i = t;
            }
        }
        
        • final修饰method可以阻止被子类覆写:
        package abc;
        
        public class Hello {
            // 无法被覆写:
            protected final void hi() {
            }
        }
        
        • final修饰field可以阻止被重新赋值:
        package abc;
        
        public class Hello {
            private final int n = 0;
            protected void hi() {
                this.n = 1; // error!
            }
        }
        
        • final修饰局部变量可以阻止被重新赋值:
        package abc;
        
        public class Hello {
            protected void hi(final int t) {
                t = 1; // error!
            }
        }
        
  • 内部类

    • **定义在另一个类的内部,所以称为内部类 **

    • 一个类定义在另一个类的内部:Inner Class

      • class Outer {
            class Inner {
                // 定义了一个Inner Class
            }
        }
        
        // inner class
        public class Main {
            public static void main(String[] args) {
                Outer outer = new Outer("Nested"); // 实例化一个Outer
                Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
                inner.hello();
            }
        }
        
        class Outer {
            private String name;
        
            Outer(String name) {
                this.name = name;
            }
        
            class Inner {
                void hello() {
                    System.out.println("Hello, " + Outer.this.name);
                }
            }
        }
        
    • 不需要在Outer Class中明确地定义这个Class,而是在方法内部:匿名类

      • // Anonymous Class
        public class Main {
            public static void main(String[] args) {
                Outer outer = new Outer("Nested");
                outer.asyncHello();
            }
        }
        
        class Outer {
            private String name;
        
            Outer(String name) {
                this.name = name;
            }
        
            void asyncHello() {
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Hello, " + Outer.this.name);
                    }
                };
                new Thread(r).start();
            }
        }
        

      观察asyncHello()方法,我们在方法内部实例化了一个RunnableRunnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:

      Runnable r = new Runnable() {
          // 实现必要的抽象方法...
      };
      

      观察Java编译器编译后的.class文件可以发现,Outer类被编译为Outer.class,而匿名类被编译为Outer$1.class。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer$1Outer$2Outer$3……

    • static nested class

      • public class Main {
            public static void main(String[] args) {
                Outer.StaticNested sn = new Outer.StaticNested();
                sn.hello();
            }
        }
        
        class Outer {
            private static String NAME = "OUTER";
        
            private String name;
        
            Outer(String name) {
                this.name = name;
            }
        
            static class StaticNested {
                void hello() {
                    System.out.println("Hello, " + Outer.NAME);
                }
            }
        }
        

        static修饰的内部类和Inner Class有很大的不同,它不再依附于Outer的实例,而是一个完全独立的类,因此无法引用Outer.this,但它可以访问Outerprivate静态字段和静态方法。如果把StaticNested移到Outer之外,就失去了访问private的权限

  • classpathjar

    • classpath`是JVM用到的一个环境变量,它用来指示JVM如何搜索`class
      
      • 因Java是编译型语言,源码文件是.java,而编译后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,要加载一个abc.xyz.Hello的类,应该去哪搜索对应的Hello.class文件
    • classpath就是一组目录的集合,它设置的搜索路径与操作系统相关

      • 在Windows系统上,用;分隔,带空格的目录用""括起来,可能长这样:

        C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
        
      • 在Linux系统上,用:分隔,可能长这样:

        /usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
        
    • 假设classpath.;C:\work\project1\bin;C:\shared,当JVM在加载abc.xyz.Hello这个类时,会依次查找:

      • <当前目录>\abc\xyz\Hello.class
      • C:\work\project1\bin\abc\xyz\Hello.class
      • C:\shared\abc\xyz\Hello.class
    • 在启动JVM时设置classpath才是推荐的做法。实际上就是给java命令传入-classpath参数:

      java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
      
    • jar

      有很多.class文件,散落在各层目录中,肯定不便于管理;jar包就是用来干这个事的,它可以把package组织的目录层级,以及各个目录下的所有文件(包括.class文件和其他文件)都打成一个jar文件

      • jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class,就可以把jar包放到classpath中:

        java -cp ./hello.jar abc.xyz.Hello
        

        这样JVM会自动在hello.jar文件里去搜索某个类

    • 如何创建jar包?

      直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip改为.jar,一个jar包就创建成功

      假设编译输出的目录结构是这样:

      package_sample
      └─ bin
         ├─ hong
         │  └─ Person.class
         │  ming
         │  └─ Person.class
         └─ mr
            └─ jun
               └─ Arrays.class
      

      特别注意的是,jar包里的第一层目录,不能是bin,而应该是hongmingmr

      上面的hello.zip包含有bin目录,说明打包打得有问题,JVM仍然无法从jar包中查找正确的class,原因是hong.Person必须按hong/Person.class存放,而不是bin/hong/Person.class

  • class版本

    • Java 8,Java 11,Java 17,是指JDK的版本,也就是JVM的版本

      java.exe这个程序的版本:

      $ java -version
      java version "17" 2021-09-14 LTS
      

      而每个版本的JVM,它能执行的class文件版本也不同。例如,Java 11对应的class文件版本是55,而Java 17对应的class文件版本是61

    • 指定编译输出有两种方式

      • 一种是在javac命令行中用参数--release设置:
      $ javac --release 11 Main.java
      

      参数--release 11表示源码兼容Java 11,编译的class输出版本为Java 11兼容,即class版本55。

      ​ 第二种方式是用参数--source指定源码版本,用参数--target指定输出class版本:

      $ javac --source 9 --target 11 Main.java
      

      如果使用Java 17的JDK编译,它会把源码视为Java 9兼容版本,并输出class为Java 11兼容版本

    • 运行时使用哪个JDK版本,编译时就尽量使用同一版本的JDK编译源码

  • 模块

    • .class文件是JVM看到的最小可执行文件,而一个大型程序需要编写很多Class,并生成一堆.class文件,很不便于管理,所以,jar文件就是class文件的容器
    • jar只是用于存放class的容器,它并不关心class之间的依赖
      • 如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种**自带“依赖关系”的class容器就是模块**