前言
本文从一个 JS/TS 的开发者角度,记录 Java 学习过程中一些陌生/与 JS 类似但细节不一样的地方。
基本结构
每一个 .java 文件都是一个类,类名与文件名相同,例如针对 Person.java 文件,类名应该为 Person。
class Person { // ...}而对于其入口文件,则要求必须使用 public 修饰符。且必须包含一个名称固定为 main 的方法,作为程序的入口,这个方法需要由 public static void 修饰。
public class Person { public static void main( String[] args ) { // ... }}一个 .java 文件中可以编写多个类,但是只能有一个 public 类,其他类不能使用 public 修饰符,
这些修饰符将在后面说明。
面向对象
一个 .java 文件内有且只能存在一个 public 类,且需要与文件同名
一个类默认情况下是自动继承 Object 类,Object 类是所有类的父类。
class Person extends Object { // ...}方法
定义方法的语法是:
修饰符 方法返回类型 方法名( 方法参数列表 ) { 若干方法语句; return 方法返回值;}例如:
class Person { public void sayHello() { System.out.println( "Hello" ); }}可变参数
可以通过 ... 修饰符来表示可变参数,这样可以接受任意个数的参数。方法内接收到的参数为一个数组
class Person { public void sayHello( String... names ) { for ( String name : names ) { System.out.println( "Hello " + name ); } }}
// 调用Person p = new Person();p.sayHello( "Tom", "Jerry" );方法重载
可以通过定义多个方法名相同,但参数列表不同的方法来实现方法重载。
class Person { public void sayHello() { System.out.println( "Hello" ); } public void sayHello( String name ) { System.out.println( "Hello " + name ); }}
// 调用Person p = new Person();p.sayHello(); // Hellop.sayHello( "Tom" ); // Hello Tom构造函数
构造函数没有返回值,也不需要手动添加类型 void,起函数名应与类名相同。
public 类名( 方法参数列表 ) { 若干方法语句;}默认构造函数
若一个类没有显式定义构造函数,则编译器会默认添加一个构造函数
class Person { // 默认添加 public Person() { }}构造函数重载
Java 的构造函数允许存在多个,但是参数列表必须不同。这使得允许在创建实例对象时传入不同的参数来实现函数重载。
class Person { public Person() { System.out.println( "无参构造函数" ); } public Person( String name ) { System.out.println( "有参构造函数" ); }}
Person p1 = new Person(); // 无参构造函数Person p2 = new Person( "Tom" ); // 有参构造函数继承
在发生类继承后,若子类没有显式定义构造函数,则会默认添加一个构造函数,并在其中调用父类的无参构造函数。
class Student extends Person { Public Student() { // 默认添加 super(); }}阻止继承
当一个类被 final 修饰时,该类不再允许被继承
final class Person { // ...}限制继承
Java 15 开始,允许使用 sealed 修饰 class,意为密封类,被修饰的类要求必须存在继承类。
使用 sealed 修饰后,可以通过 permits 来限制允许继承的子类名称
public sealed class Person permits Student, Teacher { // ...}被指定允许继承的子类在继承时,必须由 non-sealed 或 final 或 sealed 修饰
public non-sealed class Student extends Person { // ...}函数覆写
当子类中定义了一个与父类中名称相同、参数列表相同、返回值类型相同的方法时,称为函数覆写。
class Person { public void sayHello() { System.out.println( "Hello" ); }}
class Student extends Person { @Override public void sayHello() { System.out.println( "Hello Student" ); }}对于注解
@Override,它是可选的,但是建议使用,因为它可以帮助我们检查是否真的覆写了父类中的方法。
若方法不希望被子类覆写,可以使用 final 修饰符
class Person { public final void sayHello() { System.out.println( "Hello" ); }}向上转型
如果一个类继承了另一个类,那么可以将子类的实例赋值给父类的引用变量,这种操作称为向上转型。
Person p = new Student();Object o = new Student();当然,如果使用了向上转型,那么只能调用父类中存在的方法,不能调用子类中独有的方法(若子类中存在对应的函数复写,实际调用的依然是子类的方法,详见多态)
向下转型
向上转型是可以自动实现的,而向下转型则需要手动强制转换。只允许对使用向上转型的对象进行向下转型。
Person p = new Student();Student s = (Student) p;若一个实例对象本身就是使用父类构造函数创建的,则强制进行向下转型会得到一个 ClassCastException 异常。
Person p = new Person();Student s = (Student) p; // ClassCastExceptioninstanceof
为了避免强制向下转型时出现错误,可以使用 instanceof 运算符来判断一个对象是否是某个类的实例。
Person p1 = new Student();Person p2 = new Person();
System.out.println( p1 instanceof Student ); // trueSystem.out.println( p2 instanceof Student ); // falseJava 14 之后,使用 instanceof 进行条件判断以后,可以直接转型为指定名称的变量,避免再次强制转型。
例如下面这个示例,使用 Object 声明的变量显然是无法使用 Person 类独有的 echo() 方法的。此时我们往往需要进行如下操作:
Object obj = new Student();if ( obj instanceof Student ) { ( (Student) obj ).echo();}而借助 Java 14 的新特性,我们可以直接将 obj 转型为 Student 类型的新变量 stu:
Object obj = new Student();if ( obj instanceof Student stu ) { stu.echo();}多态
针对函数覆写中的示例,我们有如下调用:
Person p = new Student();p.sayHello(); // Hello Student可以看到,虽然 p 变量的引用类型为 Person,但实际调用的却是实际类型 Student 中覆写过后的方法。
通过这一点可以得到结论:Java 的实例方法调用,是基于运行时的实际类型的动态调用,而非声明时的类型。而这个特性在 oop 中被称作“多态”
TS 中也有类似的实现方式
class Person {sayHello() {console.log( "Hello" );}}class Student extends Person {sayHello() {console.log( "Hello Student" );}}const p: Person = new Student();p.sayHello(); // Hello Student
静态属性与静态方法
这两个可以通过类名直接访问,这点是毋庸置疑的。但在 Java 中,实例对象同样可以访问静态属性和静态方法。
class Person { static int age = 18; static void sayHello() { System.out.println( "Hello" ); }}
// 调用Person p = new Person();System.out.println( p.age ); // 18p.sayHello(); // Hello因为当使用实例对象去访问静态属性时,Java 编译器实际上自行将其转换为了 类名.静态属性 的方式。
尽管可以这样,但是依然只推荐使用类名去访问静态属性和静态方法。
接口
如果一个抽象类中的所有方法都是抽象的,那么这个抽象类可以转而使用接口代替。
interface Person { void sayHello();}
class Student implements Person { @Override public void sayHello() { System.out.println( "Hello Student" ); }}接口中所有的方法都是默认自带 public abstract 修饰符的,编写时不需要手动添加。
接口中的属性
而在接口中,是可以定义属性的,此时必须要提供初始值
interface Person { int age = 18;}这些接口中的属性默认是 public static final 修饰的,不需要显式注明这些关键词。
因此在类继承接口后,类将会拥有这些静态属性,可以直接通过类名访问。
作用域
JAVA 中修饰作用域的关键字有:public、protected、private。
public:任何地方都可以访问,不同包的类也可以访问protected:仅允许继承的子类访问private:仅允许本类访问
而当什么都不修饰时,表示为包内作用域,只允许相同包的 class 访问。包的概念在下文将会提及。
包
Java 中定义了一种命名空间,称为包(package),这也是 Java 自己的模块化支持(对比 ESM)。
包的定义是在 Java 源文件的第一行,使用 package 关键字定义。
package com.example;
class Person { // ...}这里我们定义了包为 com.example,但不止是这样就结束了,与文件名和类名的关系一样,包名也要严格遵守目录结构。
因此我们还必须把这个 .java 文件放在 com/example 目录下。
需要注意的是,反过来同样也是严格要求的,即出现在 com/example 目录下的 .java 文件,其必须通过 package 修饰符手动定义包名为 com.example。
也就是说,包名和目录结构是互相对应的。
包没有父子关系,
com.mari和com.mari.doc完全没有任何关系
包内作用域
在 com/example 目录下,我们可以继续定义其他的类,这些类都将会被归类到 com.example 这个包下,相互之间可以自由访问,这被称作为包内作用域。
如下示例,同包下的两个类是可以互相访问的。
package com.example;
public sealed class Person permits Student { void sayHello() { System.out.println( "Hello" ); }}
// com/example/Student.javapackage com.example;
public non-sealed class Student extends Person { @Override void sayHello() { System.out.println( "Hello Student" ); }}而没有使用 public、protected、private 修饰的类、方法、属性,它们的作用域是包内的,只有在同一个包下的类才能访问。
也因为这个特性,同一个包内不可以定义两个相同名称的 class,哪怕没有使用 public 修饰。
package com.example;
public class Main { public static void main( String[] args ) { Student stu = new Student(); stu.sayHello(); // Hello }}上面这个示例中,同包内的 Teacher 类可以访问 Student 类,且能够调用 Student 类内部未使用任何修饰符修饰的 sayHello() 方法。
跨包访问
当有时候我们需要使用不在本包作用于的 class,例如我们在 com.mari 包下的 Food 类中,希望访问 com.example 包下的 Student 类。
一种方法是直接写出完整类名:
package com.mari;
public class Main { public static void main( String[] args ) { com.example.Student stu = new com.example.Student(); }}可以看出,不同于同包下的直接使用 Student,这里需要写出完整的类名 com.example.Student。显然这种写法有些过于繁琐了。
因此我们可以使用另一种办法: 使用 import 关键字引入包名。
package com.mari;// 引入完整包名import com.example.Student;
public class Main { public static void main( String[] args ) { Student stu = new Student(); }}不过如果在两个不同的包内存在同名类,我们还都希望使用的话,只能对其他的使用完整类名了。
如果希望引入包内的所有内容,可以使用 * 通配符。
package com.mari;// 引入包内所有内容import com.example.*;
public class Main { public static void main( String[] args ) { Student stu = new Student(); Person p = new Person(); }}但通常情况下不建议使用这种写法,因为不够直观,在导入了多个包后无法看出使用的类来自哪个包。
在跨包的情况下,我们只能使用 public 修饰的类、方法、属性。针对 包内作用域 中例子,此时我们将无法访问没有添加任何修饰符的 sayHello() 方法。
package com.mari;// 引入包内所有内容import com.example.*;
public class Main { public static void main( String[] args ) { Student stu = new Student(); stu.sayHello(); // Error }}编译器分析步骤
Java 编译器在遇到一个类名时,会按照以下模式进行分析:
- 如果是完整类名,直接按路径查找
- 如果是简单类名,则分为几个步骤依次查找:
- 在当前包内查找
- 在
import引入的包内查找 - 在
java.lang包内查找
当按照这种模式全部无法找到目标类,则抛出错误。
为此我们可以看出 Java 编译器自动为我们实现了两个 import 动作:
import 当钱包.*;import java.lang.*;如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时