面向对象基础
理解:
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类的name和age字段: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! } }supersuper关键字表示父类(超类)。子类引用父类的字段时,可用super.fieldNamepublic 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出现在Shape的permits列表中。但是,如果定义一个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输出为Stringequals():判断两个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修饰的方法不能被Overrideclass 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.PersonJDK的
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定义为
public的class、interface可以被其他任何类访问:package abc; public class Hello { public void hi() { } }上面的
Hello是public,因此,可以被其他包的类访问:package xyz; class Main { void foo() { // Main可以访问Hello Hello h = new Hello(); } }private定义为
private的field、method无法被其他类访问: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(); } } }
protectedprotected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类:package abc; public class Hello { // protected方法: protected void hi() { } }package xyz; class Main extends Hello { void foo() { // 可以访问protected方法: hi(); } }
package包作用域是指一个类允许访问同一个
package的没有public、private修饰的class,以及没有public、protected、private修饰的字段和方法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()方法,我们在方法内部实例化了一个Runnable。Runnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:Runnable r = new Runnable() { // 实现必要的抽象方法... };观察Java编译器编译后的
.class文件可以发现,Outer类被编译为Outer.class,而匿名类被编译为Outer$1.class。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer$1、Outer$2、Outer$3……static nested classpublic 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,但它可以访问Outer的private静态字段和静态方法。如果把StaticNested移到Outer之外,就失去了访问private的权限
classpath和jarclasspath`是JVM用到的一个环境变量,它用来指示JVM如何搜索`class- 因Java是编译型语言,源码文件是
.java,而编译后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,要加载一个abc.xyz.Hello的类,应该去哪搜索对应的Hello.class文件
- 因Java是编译型语言,源码文件是
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.Hellojar包有很多
.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,而应该是hong、ming、mr上面的
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容器就是模块**
- 如果