LOADING
7560 字
38 分钟
Java面向对象基础随记
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello World");
}
}

面向对象基础#

class和class有关的名词#

访问修饰符#

public就是公用的声明,叫做访问修饰符吧,这个就是公用的rbq,可以在JVM里被任意地调用出来,访问修饰符有点 就近原则的感觉,就算你在公类 private 一个 field(字段),那个 field肯定就不能被调用了

但问题就来了,如果不能在外面被操纵,那 private的意义何在?所以方法由此诞生,方法使得对象中的 field可以被更安全地只被该类定义的“行为模式”给修改,这便是方法

new(实例化)#

malloc的方法初始化版,对象创建的一个必要操作 new除了会开辟一块新的内存,还可以基于参数去调用构造函数,把对象的属性设置好

  • 问:如果直接像基本类型一样去使用类,而不去用 new,会发生什么呢?
    • 答:只是分配了一个用于引用的32bits或者64bits的空间,指向的为null,调用会出错

方法#

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

main就是一个典型的方法,方法的意思就是类里面的函数,这里的 main其实还是有点类似于C的作用,是一个.class文件被JVM运行的入口,JVM在运行整个java项目的时候首先就会先去找它,然后再逐行执行,也就是说:

  • 除了入口方法 main,其他方法的名字都可以随意。而且这个main其实可以随便放在任意一个平行类里,而且也不只可以放一个,如果你放了不只一个class的话,那么编译过程中就会产生多个 .class文件,在JVM你可以选择任意一个进行实际的运行

this变量#

一个始终指向当前实例的隐藏变量,可以访问对象的所有 field

可变参数#

第一个 语法糖 一种让代码更简洁的重构方式

public void setNames(String... names)
public void setNames(String[] names)
  • 这两个语句都代表方法所需要的传参为一个数组,但是对与第二条语句,也就是所谓的 可变参数,如果你传参写了 一堆Str的单元素变量,比如 "Alice", "Bob",那他也会自动打包
  • 另外,传统数组参数可接受NULL,但是可变参数并不会允许这样,当 没有传参时,会构建一个0元素的变量

构造方法#

  • 一个连返回值都不配拥有的方法
  • 甚至名字都是和类名完全相同,寄生虫啊我看
  • 作用就是在 new的时候初始化内部的各种字段,初始化到 合适的位置,避免在各种情况没有调用类的某些参数时会错误返回到一个 不合理 的情况

方法重载#

  • 当方法定义的形参不同的时候,传入不同数量的参数可以调用不同的
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;
}
}
  • 而且你没有发现吗,所有对象在new的时候后面跟着的写法 不正是一个同名的方法吗?? ,这么重要的信息居然被你忽略了

  • 总之就是方法重载就等于是新增了一个可变的方法(这一点在后面的方法重载也会有体现),最重要的一点是:你可以让这个同名方法形参类型,个数,顺序不一样(一般都是这样),同时你也可以让这个方法返回值不一样 ==但你不能说形参一模一样的同时还换了返回值==,那这样JVM会说:奶奶的,你到底想让我返回哪个类型啊,给个准信

  • (BTW其实这算不合法的重写,跟你私自缩小重写方法权限会失败是同一个道理,只要你方法名相同,传参类型顺序相同,编译的时候就会自动进入重写方法格式检测)

  • 但也因此这个也会产生一个特例:除非如果父类的方法是返回一个父类,子类可以把这个同名方法返回一个更小类,这个其实是合法的,去看 向上转型那一章就能够知道,返回的实例是子类写的方法返回值还是父类的返回值都是合法的,这就是 协变返回类型

    class A {}
    class B extends A {}
    class Parent {
    A getObj() { return new A(); }
    }
    class Child extends Parent {
    @Override
    B getObj() { return new B(); } // ✅ 合法(协变返回)
    }

构造方法可以有多种#

不同参数数量和类型,java编译器能够自动匹配对应的构造方法 构造方法可以这样,其他方法也当然如此,这就是 方法重构

int indexOf(int ch) //根据字符的Unicode码查找;
int indexOf(String str) //根据字符串查找;
int indexOf(int ch, int fromIndex) //根据字符查找,但指定起始位置;
int indexOf(String str, int fromIndex) //根据字符串查找,但指定起始位置。

继承extends#

当一个新类有很多成员方法完全与某一个其他类一样,避免复制粘贴,所以使用继承

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) { … }
}

所有没有写extends的类,都其实继承了Obeject

使用enxdtend后,具有以下默认的属性

  • 子类无法直接访问private字段和private方法,如果某个父类需要被继承,那可见性改为protected会更好一点,protected可以被所有继承树上的类或者对象给引用or修改,严格符合继承树
  • 子类引用父类的字段时,可以用super.fieldName
  • java规定任何类的构造方法 第一条的代码 要么是调用父类的构造方法,要么是调用自己已声明的构造方法this(),如果这两个都没写,则会隐式地加上super(),你可以在第一行显示的切换任意的构造重载方法
  • 也就是说如果父类没有写无参的构造方法,那么子类必须写上自己的构造方法
  • 父类的引用没有办法去调用只有子类才有的方法(原因是编译时编译器完全不会管栈上的a实际指向的是哪个,只会去声明的类里去找方法)
    Animal a = new Dog(); //legal
    a.bark(); //error

阻止继承#

有的时候,我们就是不想让类被一些小比崽子继承怎么办? 在java 15之前,只要不是被final修饰的类,都能被合理的继承 但之后,用sealed(封装的意思),就可以用permit去规定能继承的子类名称

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

向上转型#

Person p = new Student();

java允许这样的new,因为子类具有所有父类的特性 向上转型实际上是把一个子类型安全地变为更加抽象的父类型,所谓向上的上就是如此

向下转型#

由于向上转型的存在,实际上父类类型变量指向的是哪一个子类(还是它本身),在编译之前我们都不知道,父类很大概率并不具有子类的新增成员(实际上肯定不具有啊,要不然为什么要继承。),但是又指向了子类的空间,如果强转的方向是这个子类引用本身,那当然合法(实例总是能满足引用的要求),但如果强转的方向是这个子类的“兄弟”,那这个就说不通了(子类一般都会加东西,但是兄弟类型和本类同时加东西的话,兄弟加的东西和此类会完全一样吗?大概率不是吧,所以就会导致实例(赋值等式右边)无法满足引用类型的要求,向下转型失败)

只是理解层面,实际上更复杂一点:

转型失败不是因为“字段不同”,而是因为对象的实际类型不兼容(JVM 的类型标记不同)。 即使两个类结构一模一样(比如字段名和数量完全相同),只要类型系统中它们无继承关系,仍然不行。

instanceof#
  • a instanceof b = a是b的实例吗? 建立在向下转型的基础,不能强转的就是false

  • 继承后,是 is 关系,如果你的逻辑两个类是 has 关系,那么应该用组合(就是把类当成员),而不是继承

多态#

Override#

其实就是子类定义了一个 与父类完全相同的方法 ,方法名相同,方法参数相同,返回值也相同,做题区里最常见的Override就是在子类中重写toString,其实重写toString也不一定要用@Override,但是如果你在方法前加上这个声明的话,那么编译器会帮你检查拼写,可见性和方法参数的错误(它人还怪好的咧),那么这一个你早就用烂的机制和这个父标题有什么关系呢?

  • 那么你能发现,其实JAVA调用同名方法的时候,其真正执行的方法取决于运行时期实际类型的方法,这种模式就叫 多态 这样说可能还是很抽象,让我们具体一点 定义三个类都有名为run的方法
class Person {
void run() {
System.out.println("人慢慢跑");
}
}
class Student extends Person {
void run() {
System.out.println("学生飞快地跑");
}
}
class Teacher extends Person {
void run() {
System.out.println("老师稳稳地跑");
}
}
Person p1 = new Person();
Person p2 = new Student();
Person p3 = new Teacher();
p1.run(); // 人慢慢跑
p2.run(); // 学生飞快地跑
p3.run(); // 老师稳稳地跑

以上结果充分表明:“你是 Person 的一员,就应该有 run 方法。”,如果将来要新增一个子类,它也能自动正确计算,这就是可扩展性——多态带来的最大好处。允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码

来看一个更有实践意义的例子:

首先我定义了一个报税类

class Income {
protected double income;
public double getTax() {
return income * 0.1; // 税率10%
}
}

对于工资收入,可以减去一个基数,那么我们可以从Income派生出SalaryIncome,并覆写getTax():

class Salary extends Income {
@Override
public double getTax() {
if (income <= 5000) {
return 0;
}
return (income - 5000) * 0.2;
}
}

如果你享受国务院特殊津贴,那么按照规定,可以全部免税:

class StateCouncilSpecialAllowance extends Income {
@Override
public double getTax() {
return 0;
}
}

接着,你给一个有普通收入、工资收入和享受国务院特殊津贴的小伙算税,那么你的文件就可以这样写:

// Polymorphic
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;
}
}

这样总算税的方法只需要和Income一个类打交道,而无须知道其它子类的存在,非常方便你去增删其它的工资类

覆写Object(超大类)#

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

  • toString用于把实例转化字符串
  • equals()用于判断是否逻辑相当
  • hashCode()计算实例哈希值

根据所要求重现就行,除了equal都比较好理解

// 比较是否相等:
@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;
}
//慢慢去理解

调用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可修饰方法#

被修饰的方法不允许子类重写

final以修饰类#

被final修饰的类不允许任何的继承

final可以修饰字段#

被修饰的字段初始化(构造)后就不允许任何的引用更改,这里的初始化包括构造方法

  • 如果字段是引用类型:final仅保证引用本身不可变(指向不可变),不保证引用对象的内容不可变

抽象#

抽象方法#

如果一个父类方法完全不需要规定任何内容,就比如记忆命途的忆灵出伤模式,大记忆类完全不知道具体是怎么出伤的,只能由具体子类去描述,瑕蝶长夜月是自爆忆灵出伤,阿格莱雅和记忆主是驻场出伤,那么这个记忆命途的void damage方法完全不需要写这个东西,只需要:

abstract class memory {
int hp;
int power;
...
public abstract void damage(); //仅仅是为了定义方法签名,目的是让子类去覆写它
}

抽象类#

如果一个类里面有抽象方法,那么它必须也得被描述为抽象类,因为它包含了抽象方法,所以它必定无法被“实”例化,只能用于被继承如果理解不了,想象一下你能弄出一个“记忆”的角色吗?

  • 但反过来讲,抽象类没说必须要有抽象方法,只有属性的类也可以说是抽象方法
  • 抽象类可以有构造方法,也是类里唯一一个不属于abstract的方法,毕竟本质继承,如果构造方法都没有,怎么去符合super()的规范?
  • 子类如果存在没重写的方法,那么继承自带abstract前缀,那么这个子类 也应当为抽象类

抽象类不能被实例化 抽象类不能被实例化 抽象类不能被实例化 重要事情说三遍

面向抽象编程#

定义抽象父类后,所有的引用对象标签都为该抽象类(构造方法用子类),就算这个子类并不存在,也可以通过抽象的方法去避免编译错误,比如昔涟其实没新建文件夹,但是她肯定会有忆灵出伤的方式,所以在版本前内容有需要的话也可以直接用

memory x = new xilian();
x.damege(1e9);
...

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。

接口#

当有一个类只有抽象方法,连自己的字段,构造方法都没有,那么他的抽象可以被修饰成interface(接口)

  • 接口定义的所有方法默认都是 public abstract 所以修饰符可以不用写

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

    这样就足矣

  • 接口的所有属性都会默认加上public static final,也就是说必须声明同时去赋值

当一个类想要去实现一个接口的方法时,需要使用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;
}
}
  • 不一定要实例类,抽象类也一样可以implement接口,只不过方法还是抽象的就是了
  • 实现也可以使用 空方法 , 只是不够优雅

JAVA确实只能单继承一个父类,但是却能implements无数个接口,这就是接口的必要性 但是要注意,若子类implements,那必须严格重写全部该接口类声明的全部方法

接口与instanceof和引用类型#

这个对象的实际类是否实现了该接口?

如果实例确实能够实现,那么这个引用类型会自动可具有该接口名,也相当于是一种逻辑上的“继承关系”,也就是说,该实例 可以完全合法地向上转型

interface Flyable {
void fly();
}
class Bird implements Flyable {
public void fly() { System.out.println("Bird flies"); }
}
Flyable f = new Bird();
System.out.println(f instanceof Bird); // ✅ true
System.out.println(f instanceof Flyable); // ✅ true
System.out.println(f instanceof Object); // ✅ true
Bird b = new Bird();
Flyable f = b; // ✅ 合法,自动向上转型

implement实际上是一种can-do关系,可以简易地这样理解:

class Bird implements Flyable, Eatable, Drawable { ... }
/*
Bird → 可以 Flyable
Bird → 也能 Eatable
Bird → 还能 Drawable
*/

接口的菱形继承#

接口继承#

一个接口类可以继承另一个接口类,方法如下,相当于扩展了接口方法,很容易理解不细说了:

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

继承关系#

有时候总是在别人的项目部署文章里提到xxx接口什么的 对于一个符合规范的代码项目来说:实例化对象永远只能是某个具体的子类,但引用时应当只通过接口

default方法#

但是要注意,若子类implements,那必须严格重写全部该接口类声明的全部方法

由此可知,如果在接口类新增一个方法,那么子类必须全部重写,否则报错,这样会有非常大的痛苦 所以Java8可以给接口的抽象方法新增一个default修饰符,如果新增的是deault方法,所继承的子类就无须全部修改

default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

静态(static#

放在方法区

静态字段#

没有被static修饰的字段被称为实例字段,而被static定义的字段被称为静态字段,静态字段不属于任何实例,所以无论修改哪个实例的静态字段,它都会被修改

  • 也正因为如此,接口类也可以去拥有一个静态字段,结合前面所学,我们也能知道在接口类的字段是也只能是public static final(不需要去任何实例化,它在类加载阶段即被初始化,存在于方法区,可随意任何实例中被访问但不可被修改的字段),就算你不写,字段也会被编译器自动加上该修饰符

不推荐用“实例.静态字段”去访问静态字段,就算你这样写了实际也会被JVM强转,更推荐使用“类名.静态字段”

静态字段命名规则一般是全大写

就算引用变量为null,它依然能访问作用域所有的static字段

静态方法#

调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。静态方法类似其它编程语言的函数,这也是为什么main方法总是需要一个static修饰的原因,只有这样JVM才能在最开始 没有创建任何对象,也不知道构造方法、成员字段等信息 的情况下去生成一个命令行参数数组(也就是String[] args),传递给main方法

工具类常用静态方法,所以写起来不用new来new去的 其实工具类是什么也需要你去详细阐述

  • Arrays.sort()
  • Math.random()
工厂方法(静态的构造方法(伪))#

构造方法也可以设为static 那么问题来了,构造方法的作用是初始化对象,既然static可以独立于对象而存在,那这个方法何意味?所以至少在官方文档中,static绝对不可以用于构造器中,但是,我们可以依然把返回值为该类引用的方法设为static,把构造器私有化,在创建实例的时候不使用new(关键),实现这个逻辑:

class User {
private String name;
private int age;
// 构造器设为私有,防止外部随便 new
private User(String name, int age) {
this.name = name;
this.age = age;
}
// 工厂方法:静态“伪构造器”
public static User create(String name, int age) {
if (age < 0 || age > 150) {
System.out.println("年龄非法,返回 null");
return null;
}
return new User(name, age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
public class Main {
public static void main(String[] args) {
User a = User.create("Alice", 20);
User b = User.create("Bob", -3); // 不会被创建
System.out.println(a);
System.out.println(b); // null
}
}

名字就是static的方法:#

静态初始化块

static {
System.out.println("静态块执行!");
}

为什么需要它?: 会在类加载时由 JVM 自动执行一次,和构造方法第一句的super()原理类似

#

其实你在写练习的时候就发现了,如果像C那样一堆源文件放一起,java还是会整个编译,若出现同名的类就会报错,非常的不方便

但这其实是为了大型项目的管理而做的牺牲

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

包没有父子关系。虽然为了方便归类整理,会有多级的包用多个.链接表示,但java.util和java.util.zip是不同的包,两者没有任何继承关系,包之间是独立的命名空间,父包与子包仅存在现实理解的逻辑上下级关系

那是因为如果你在.java文件前没有写package包声明,那么它就会被分配到一个无名默认包,不能被任何其它源文件import

import#

那么如果要用别人包(比如说就是官方写的)这些类,按照规范,肯定得是包名.类名,如java.util.Scanner sc = new java.util.Scanner(System.in);, 这种方式也叫全限定类名,它的功能非常强大,可以直接访问任何 java 包(乃至你能访问到的任何包)里的公开字段、方法或类成员(System.in的输入流(InputStream)类型其实也是这样实现的的)。但是每次都得这样写未免也太折磨,import 它有二+一种写法

  • 只有public类能被import
    • 只有public类能被import
      • 只有public类能被import
  • 只导入包中某个类名:import mr.jun.Arrays;,这样后面使用这个类就可以只写简单类名了
  • 直接把全包的类全导入:import mr.jun.*;,一般不推荐这种写法,容易分不清哪个类是哪个包
    • 既然谈到类重名这个问题了,那么可以看一下如果发生这种情况,java编译器会怎么排优先级
      1. 当前类所在的包(当前文件的 package)
      2. 当前类的内部类或嵌套类
      3. 通过 import 明确导入的类
      4. 通过通配符 import xxx.* 导入的类
      5. java.lang 包下的类(自动导入)
    • 总之确实是:“显式优于隐式”

为了方便你编写最基本的程序,最开始编译的时候,编译器会自动帮我们做两个import动作:

  • 默认自动import当前package的其他class
  • import java.lang.*

跨包继承#

继承可以跨包,子类可以访问父类的public

模块(module)#

这个是JDK9之后才有的东西,简单来说就是包的包

和IDEA以及Maven的module不是一个概念!!!!,Maven为了区分也改成subproject了

一个月前更新了JAVA25,它支持一个非常逆天很像python的语句

import module java.base;

可以把这个包的包全部都import进去,不用一个一个去import类 虽然现在大佬都对这个感到非常匪夷所思,但其实这个又确实是我目前对module最深刻的感知了…… 但实际上,module(JPMS)在 JDK 9 → 24 期间存在的意义,从来就不是让你“更省事地 import 类” 而是让你“更安全、更清晰、更可控地管理模块间依赖(.jar与.jar之间的关联)和封装。 暂时先跳过这块吧,先有个概念

内部类#

上面说的类,基本都是平行关系,好像类与类之间的关联也只能是继承与非继承。但实际上还可以做得更多,“类中类”这样的形式还是存在的 但是目前没有使用场景,先复制一下跳过,等有需要了再来看

类型是否静态是否有名字是否能访问外部类实例成员常见用途
成员内部类表达从属关系(如 Map.Entry
静态内部类辅助类 / Builder 模式
局部内部类✅(方法内)封装方法局部逻辑
匿名内部类回调、接口临时实现

匿名类可以再稍微说一下:

  • 匿名类就是一个“没有名字的类定义”,在定义的同时立即实例化,继承一个父类并且重写一个方法,而且能够调用一次
    new 父类名或接口名([构造参数]) {
    // 重写父类或接口中的方法
    };
  • 内部类可以完全自由地去访问外部类的全部成员,包括provite

杂项#

引用值与引用对象引用名#

  • 引用名变量 名,可以说相当于 c中指针变量名的意思
  • 引用值:实际存放的地址
  • 引用对象 :实际存在里的数据

缺省就是默认的意思

局部变量#

在方法内部定义的变量称为局部变量,局部变量作用域从变量声明处开始到对应的块结束。方法参数也是局部变量。

类路径(classpath)#

不要去想复杂,就是再shell中告诉jvm这次的java命令该去哪里找类,如果你不写-cp指定类路径,我就假定你要从当前目录下找类(相当于classpath = .),所以你第一次写的文件也是能成功,现在用IDEA自动配置-cp,或者用vscode的run编译在缓存的临时文件里,都是好方法,这个概念只要知道有就行了

.jar#

最原始的jar包就是把一大堆散落的类路径压缩成zip再改名成.jar,最原始的目的也就是和上面一样方便找.class 但是很多时候像logisim.jar,一个几十M的文件却能像环境变量一样去调用出那么多的指令,这是如何做到的?:

有点复杂我直接复制GPT的了,反正也是末尾


🧩 一、什么是 .jar#

.jar = Java ARchive 本质上它是一个 ZIP 压缩包,里面打包了:

  • 编译后的 .class 文件;
  • 所有依赖资源(图片、配置、元数据);
  • 以及一个特殊的文件:META-INF/MANIFEST.MF(清单文件)。

你可以直接用解压工具打开 .jar 看内容 👇

logisim.jar
├── META-INF/
│ └── MANIFEST.MF
├── com/
│ └── cburch/
│ └── logisim/
│ ├── Main.class
│ ├── gui/
│ └── circuit/
├── icons/
└── resources/

🧠 二、.jar 的启动原理#

当你执行:

Terminal window
java -jar logisim.jar

JVM 实际上会:

  1. 先解压 jar 的头部,读取里面的 META-INF/MANIFEST.MF

  2. 找到其中的一行:

    Main-Class: com.cburch.logisim.Main
  3. 然后相当于执行:

    Terminal window
    java -cp logisim.jar com.cburch.logisim.Main

也就是说:

.jar 文件其实就是帮你自动配置了 -classpath 和主类(Main-Class)。


🧩 三、Manifest 文件详解#

清单文件路径固定:

META-INF/MANIFEST.MF

示例:

Manifest-Version: 1.0
Main-Class: com.cburch.logisim.Main
Class-Path: lib/jhall.jar lib/somelib.jar
字段含义
Manifest-Version清单格式版本
Main-Class指定程序入口类(必须有 public static void main
Class-Path附加依赖 jar 的相对路径
Implementation-Version可选,版本号信息
Created-By可选,打包工具信息

🧩 四、如何手动创建一个可执行 JAR(例如 logisim.jar)#

假设有这些文件:

Main.java
utils/Helper.java

编译:

Terminal window
javac -d out Main.java utils/Helper.java

创建清单:

echo Main-Class: Main > manifest.txt

打包:

Terminal window
jar cfm myapp.jar manifest.txt -C out .

运行:

Terminal window
java -jar myapp.jar

✅ 效果:

JVM 会自动找到 Main 类并执行它的 main() 方法。


🧩 六、.jar-classpath 的关系#

当你用 -jar 启动时,JVM 会忽略命令行的 -classpath 参数。 为什么?因为它会优先读取 Manifest 里的 Class-Path

所以这两种写法的本质区别是:

命令说明
java -jar logisim.jarMETA-INF/MANIFEST.MF 启动,忽略外部 classpath
java -cp logisim.jar com.cburch.logisim.Main自己手动指定入口类(不依赖 Manifest)

🧩 七、IDEA、命令行与 JAR 的关系#

场景启动方式classpath 来源
IDEA 运行自动生成 -classpath 参数动态拼接 out + lib
命令行 java -cp手动设置显式路径
命令行 java -jar读取 jar 内清单Manifest 的 Main-Class & Class-Path

✅ 八、总结一句话#

.jar 文件是一个带清单文件(Manifest)的 ZIP 压缩包, 里面存着 .class 文件和资源, java -jar 其实是帮你自动执行:

Terminal window
java -cp your.jar your.main.Class

并把命令行参数传给 main() 方法。


💡 一句话记忆:

.class 是单个类; .jar 是一堆类 + 启动信息的压缩包。

java -cp 是“我告诉你怎么运行”; java -jar 是“程序自己告诉 JVM 怎么运行”。


_

为获取更完整的Markdown编辑体验,你也可以直接在GitHub上编写评论

部分信息可能已经过时

Java面向对象基础随记
作者
可爱归归
发布于
2026-02-03
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
可爱归归
可爱归归的博客
公告
谢谢你能来看我OwO!
分类
标签
站点统计