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 {@OverrideB 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(); //legala.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; }}接着,你给一个有普通收入、工资收入和享受国务院特殊津贴的小伙算税,那么你的文件就可以这样写:
// Polymorphicpublic 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); // ✅ trueSystem.out.println(f instanceof Flyable); // ✅ trueSystem.out.println(f instanceof Object); // ✅ true
Bird b = new Bird();Flyable f = b; // ✅ 合法,自动向上转型implement实际上是一种can-do关系,可以简易地这样理解:
class Bird implements Flyable, Eatable, Drawable { ... }
/*Bird → 可以 FlyableBird → 也能 EatableBird → 还能 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编译器会怎么排优先级
- 当前类所在的包(当前文件的 package)
- 当前类的内部类或嵌套类
- 通过
import明确导入的类 - 通过通配符
import xxx.*导入的类 java.lang包下的类(自动导入)
- 总之确实是:“显式优于隐式”
- 既然谈到类重名这个问题了,那么可以看一下如果发生这种情况,java编译器会怎么排优先级
为了方便你编写最基本的程序,最开始编译的时候,编译器会自动帮我们做两个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 的启动原理
当你执行:
java -jar logisim.jarJVM 实际上会:
-
先解压 jar 的头部,读取里面的
META-INF/MANIFEST.MF; -
找到其中的一行:
Main-Class: com.cburch.logisim.Main -
然后相当于执行:
Terminal window java -cp logisim.jar com.cburch.logisim.Main
也就是说:
.jar文件其实就是帮你自动配置了-classpath和主类(Main-Class)。
🧩 三、Manifest 文件详解
清单文件路径固定:
META-INF/MANIFEST.MF示例:
Manifest-Version: 1.0Main-Class: com.cburch.logisim.MainClass-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.javautils/Helper.java编译:
javac -d out Main.java utils/Helper.java创建清单:
echo Main-Class: Main > manifest.txt打包:
jar cfm myapp.jar manifest.txt -C out .运行:
java -jar myapp.jar✅ 效果:
JVM 会自动找到
Main类并执行它的main()方法。
🧩 六、.jar 和 -classpath 的关系
当你用 -jar 启动时,JVM 会忽略命令行的 -classpath 参数。
为什么?因为它会优先读取 Manifest 里的 Class-Path。
所以这两种写法的本质区别是:
| 命令 | 说明 |
|---|---|
java -jar logisim.jar | 按 META-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 怎么运行”。
_
部分信息可能已经过时