# 语法

# 语法特点

  • Java 源文件的扩展名为 .java 。需要先编译成扩展名为 .class 的类文件,然后交给 JVM 运行。
  • 每个语句的末尾以分号 ; 作为分隔符。
  • // 声明单行注释,用 /**/ 声明多行注释。
  • 支持面向对象编程,但不支持定义函数。
  • Java 的语法与 C++ 相似,但是做出了许多优化,比如:
    • 丢弃了 C++ 中一些难用的功能,比如操作符重载、多继承。
    • 用引用取代了指针。
    • 自动回收垃圾内存。

# 程序示例

编写一个源文件 Dog.java :

public class Dog{                 // 定义类
    String name;                  // 定义成员变量
    int age = 0;

    public Dog(String name){      // 定义构造方法
        this.name = name;         // 通过 this 指针调用该对象的成员
    }
    public Dog(int age){
        this.age = age;
    }
    public void setAge(int age){  // 定义成员方法
        this.age = age;
    }
    public int getAge(){
        return age;
    }
    public static void main(String[] args){   // 定义主方法
        Dog dog1 = new Dog("Tony");
        dog1.setAge(2);
        System.out.println("小狗的名字为:" + dog1.name);
        System.out.println("小狗的年龄为:" + dog1.getAge());
    }
}
  • 一个源文件中可以定义多个类,但最多只能定义一个 public 类,并且该类必须与源文件同名(区分大小写),否则编译时会报错。
  • 用 Java 编程时,需要定义有且仅有一个主方法,作为程序运行的入口。
    • 当 JVM 执行一个 Java 程序时,会寻找包含主方法的类(称为 Main 类),执行其主方法。
      • 如果不存在 Main 类,则不能执行该 Java 程序,报错:Main method not found in class Main
      • 一个 Java 程序中可以存在多个 Main 类,但通常只定义一个,以简化运行逻辑。
    • 只有声明为 public static void main(String[] args) 的方法才会被视作主方法。
      • 其中形参 args 的名称可以自定义。
  • System.out.println() 用于输出一个值到终端。其中,System 是系统类,out 是标准输出对象,println() 是其中的一个方法。

# 数据类型

# 基本类型

boolean x = false;  // 存储空间占 1 bit ,取值为 true 或 false
byte    x = 0;      // 占 8 bit ,取值范围为 -128~127
short   x = 0;      // 占 2 字节
int     x = 0;      // 占 4 字节
long    x = 0L;     // 占 8 字节,赋值时要加后缀 L 或 l
float   x = 0.0f;   // 占 4 字节,赋值时要加后缀 F 或 f
double  x = 0.0d;   // 占 8 字节,赋值时的后缀为 F 或 f 。但可以不加后缀,因为浮点数默认为 double 类型
char    x = 'A';    // 占 2 字节,用于存储单个 Unicode 字符,用单引号作为定界符。取值范围为 \u0000~\uffff

# 常量

  • :在初始化之后不能改变取值。
  • 用关键字 final 声明常量。如下:
    final double PI = 3.1415927;
    

# 字符串

  • :String ,用双引号作为定界符。
  • 字符串支持通过 + 与其它类型的值自动拼接,如下:
    System.out.println("Hello" + 0.0);  // 显示为 Hello0.0
    

# 引用

  • 与 C++ 相比,Java 用引用取代了指针,用于存储对象、数组的地址。
  • 例:
    Dog dog1;           // 创建一个引用类型的变量,可用于指向 Dog 类型的对象
    dog1 = new Dog(1);  // 用 new 创建一个对象,并将引用指向它
    dog1 = new Dog(2);  // 可以改变引用指向的对象
    Dog dog2 = dog1;    // 多个引用可以指向同一个对象
    

# 数组

  • Java 中用关键字 new 创建数组,实际上是创建一个对象并返回引用。
  • 例:创建指定大小的数组,然后赋值
    int[] array = new int[3];
    array[0] = 0;
    System.out.println(array[0]);
    
    • C++ 中声名数组的格式为 int array[] ,而 Java 中为了避免歧义,建议采用 int[] array 的格式。
  • 例:在创建数组的同时初始化
    int[] array = {0, 1, 2};
    

# 泛型

  • < > 可以声明泛型(generics)。
    • 可以将数据类型参数化,允许用多种类型的值赋值。
    • 主要用途:代码中的强制类型转换可能转换成错误的类型。如果声明为泛型,在编译时会自动进行强制类型转换,从泛型转换成具体的数据类型,如果不安全则报错。
  • 例:
    import java.util.*;
    
    List<String> arrayList = new ArrayList<String>();   // 使用 String 作为类型参数
    arrayList.add("Hello");
    arrayList.add(100);   // 编译时会报错:incompatible types: int cannot be converted to String
    

# 流程控制

与 C++ 相似,Java 支持 if、switch 条件语句,和 for、while、do-while 循环语句。如下:

  • 嵌套的 if 语句:
    int x = 1;
    
    if( x > 1 ){
        System.out.println("A");
    }else if( x == 0 ){
        System.out.println("B");
    }else {
        System.out.println("C");
    }
    
  • for 循环:
    for(int x = 0; x < 10; x++) {
        System.out.println("x = " + x );
    }
    
  • 可以通过增强型 for 循环遍历一个数组:
    int [] array = {0, 1, 2, 3};
    
    for(int x : array ){
        System.out.println( x );
    }
    

#

  • Java 支持在类中、方法中定义嵌套的类,称为内部类。
    • 内部类可以直接访问外部类的私有成员。
    • 但是不支持在方法中定义嵌套的方法。

# 方法

  • 与类名同名,且没有声明返回值类型的方法会被编译器视作构造方法。
    • 每个类都默认有一个空的构造方法,其形参列表为空、内容为空。如下:
      public Dog(){}
      
    • 可以定义多个构造方法,但它们的形参列表必须不同。
    • 实例化一个类时,编译器会自动调用与实参列表匹配的那个构造方法,初始化该对象。
    • 构造方法只能在实例化类时被编译器自动调用,不能用代码直接调用。
  • Java 不支持给方法的形参声明默认值。如下:
    public int sum(int x, int y=0){   // 这样会导致编译时解析形参列表出错
        return x + y;
    }
    

# 对象

  • 用关键字 new 可以实例化一个类,返回一个对象。如下:
    Dog dog1 = new Dog(1);
    

# 继承

  • 定义类时,可以用关键字 extends 继承另一个类。
    • Java 只支持单继承,不支持多继承(一个子类同时继承多个父类)。
    • Object 类是所有类的父类。
  • 例:
    class Cat extends Dog{
        public Cat(){           // 子类不会继承父类的构造方法,需要自己定义
            super();            // 调用父类的构造方法
        }
        public Cat(String name, int age){
            super(name, age);
        }
        void sleep(){
        }
        void show(){
            super.getAge();
            this.sleep();
        }
        public static void main(String[] args){
            Dog cat1 = new Cat("Pussy");
            System.out.println("小猫的年龄为:" + cat1.getAge());
        }
    }
    
  • 通过关键字 this 可以调用当前对象的成员,通过关键字 super 可以调用父类对象的成员(直系父类)。
    • 定义构造方法时,可以通过 this、super 调用其它构造方法,但必须是方法内的第一条可执行语句。
      • 如果没有通过 super 调用父类的构造方法,编译器默认会隐式地执行 super() ,调用父类的无参数构造方法。
    • 当子类的方法名或变量名与父类重名时,编译器会优先执行子类的。
  • Java 主要有两种方式实现多态性:
    • 方法重写(override)
      • :在一个类及其子类中,如果定义多个方法,它们的名称、形参列表都相同,则只会保留最后一个定义的方法,之前的同名方法会被重写、覆盖。
      • 例如父类的一个方法,执行了某个操作。子类可以重写该方法,执行不同的操作。
    • 方法重载(overload)
      • :在一个类及其子类中,如果定义多个方法,它们的名称相同、形参列表不同,则这些方法都会保留。
      • 用户调用该名称的方法时,实参列表与哪个方法的形参列表匹配,就会调用哪个方法。

# 修饰符

  • Java 提供了多种修饰符,用于修饰某个标识符。
    • 使用时,通常将修饰符,写在被修饰对象之前。
  • 主要分为两类:
    • 访问控制符:用于修饰类,或者类中的成员,控制它们的访问权限。
    • 非访问控制符:用于修饰变量、函数、类。

# 访问控制符

  • default

    • 修饰的目标,可以从同一个包内访问。
    • 一个类或者成员,如果未使用访问控制符,则默认会当作 default 类型处理。
    • 类(非内部类)只能被 default 或 public 修饰。
  • private

    • 修饰的目标,只能从当前类中访问。
  • protected

    • 修饰的目标,可以从子类,或者同一个包中访问。
  • public

    • 修饰的目标,可以从所有位置访问。

# 非访问控制符

  • static

    • 修饰的变量称为静态变量。
      • 不管一个类创建多少个实例,只会存储一份静态变量。
      • 只能修饰类的成员变量,不能修饰局部变量。
    • 修饰的方法称为静态方法。
      • 静态方法只能访问静态成员、局部成员,不能访问非静态成员。
      • 静态方法可以通过 对象名.方法名 的形式调用,也可以通过 类名.方法名 的形式调用。
    • 不被 static 修饰的成员方法、成员变量称为非静态成员。
  • final

    • 修饰的类不能被继承。
    • 修饰的方法不能被重载。
    • 修饰的变量不能修改取值(即常量)。
      • 比如可以用 static final int id = 1; 的形式声明一个类常量。
  • abstract

    • 修饰的类称为抽象类。
      • 抽象类不能再被 final 修饰。
      • 抽象类不能直接实例化,只能被继承。
    • 修饰的方法称为抽象方法。
      • 抽象方法不能再被 final 或 static 修饰。
      • 一个抽象类中,可以包含多个抽象方法,和非抽象方法。非抽象类中,不允许包含抽象方法。
      • 当前类继承一个抽象类时,需要 override 其所有抽象方法,除非当前类也是抽象类。
      • 例:
        public abstract class SuperClass{
            abstract void sum();            // 定义抽象方法时,不能定义方法体,而且要以分号 ; 结尾
        }
        
        class SubClass extends SuperClass{
          void sum(){                       // 实现抽象方法
              return 0;
          }
        }
        
      • Java 的抽象方法类似于 C++ 中的 virtual 虚函数。

# interface

  • 接口:一种抽象类型,定义的语法像类,用关键字 interface 声明。
    • 接口没有构造方法,只包含一些抽象方法。
    • 接口中的每个方法都会被编译器隐式地用 public abstract 声明,不能再用其它修饰符。
    • 接口中的每个成员变量都会被编译器隐式地用 public static final 声明,不能再用其它修饰符。
    • 接口可以继承其它接口,支持多继承。
    • 例:
      interface Pet extends Animal{
        public void eat();
        public void sleep();
      }
      
  • 一个类可以通过关键字 implements 继承一个接口。
    • 支持多继承。
    • 类必须实现接口的所有抽象方法,除非该类是抽象类。
    • 例:
      class Dog implements Pet{
        public void eat(){
            System.out.println("eating");
        }
        public void sleep(){
            System.out.println("sleeping");
        }
      }
      

# annotation

  • Java 支持用 @ 给类、方法、变量、包等对象添加注解(Annotation),实现比注释更多的功能。

    • 所有注解都继承于 java.lang.annotation.Annotation 接口。
  • 例:添加注解

    @MyAnnotation1("Hello")
    public class Hello {
        @@MyAnnotation2(length=32)
        String name;
    
        @Override
        public String toString() {
            return "Hello";
        }
    }
    
  • 例:定义注解

    @Documented                         // 将该注解加入到 javadoc 中
    @Target(ElementType.TYPE)           // 声明该注解的类型。ElementType.TYPE 表示可以修饰类、接口(包括注解类型)、枚举
    @Retention(RetentionPolicy.RUNTIME) // 声明该注解的保留策略。RetentionPolicy.RUNTIME 表示保留到类文件中,支持 JVM 在运行时读取
    public @interface MyAnnotation1 {   // 用 @interface 定义一个注解,名为 MyAnnotation1
        String value() default "";
    }
    
    • 元注解:用于修饰注解的注解。
      • 定义注解时,上方可以添加一些元注解来修饰它。
    • 可以在注解中定义任意个参数。
      • 如果参数名为 value ,则传参时可以省略参数名。
      • 可以用 default 关键字给参数声明默认值。
  • Java 内置的注解举例:

    @Override           # 只能修饰方法,表示该方法重写了父类的方法
    @Deprecated         # 表示该对象被弃用。如果调用了该对象,则编译时会产生警告
    @SuppressWarnings   # 编译时,屏蔽该对象产生的警告
    

# package

  • 用关键字 package 可以将一个源文件的内容加入到一个包(package)中。

    • package 语句必须是源文件中的第一条可执行语句。
    • 例:
      package test.animals;
      
      public class Dog{
          ...
      }
      
      • 包名通常全部小写。
      • 包名必须与源文件的存储目录名一致,否则 JVM 会找不到该包对应的源文件。例如上例中源文件的存储路径应该是 test/animals/Dog.java
      • 一些公司通常以域名作为所有包的根目录,例如 com.sun.javadoc.ClassDoccom.sun.javadoc.MethodDoc
    • 每个包有自己独立的命名空间,类似于 C++ 中的 namespace 。
      • 例如:同一个包中,类名不能重复。不同包中,类名可以重复。
  • 用关键字 import 可以导入一个包的内容。

    • import 语句必须放在 package 语句之后、类声明之前。
    • 例:
      import test.animals.Dog;  // 导入一个包中的指定类
      import test.animals.*;    // 可以使用通配符 * 导入包中的所有成员
      
    • Java 程序默认会隐式地导入 java.lang 包。

# 异常处理

  • 异常(Exception)和错误(Error)严格来说是两种概念。
    • 错误
      • :会导致程序停止运行。
      • 程序可以捕捉并处理自己的异常,但处理不了错误。
    • 检查性异常
      • :在编译时就能检查出来,会导致编译不能通过。
      • 通过继承 Exception 类,可以自定义一个检查性异常类。
      • 所有异常类都是 java.lang.Exception 类的子类。
    • 运行时异常
      • :编译可以通过,在程序运行时才能发现。
      • 通过继承 RuntimeException 类,可以自定义一个运行时异常类。
  • 可以用关键字 throw 主动抛出一个异常,用关键字 throws 声明某个方法可能抛出某些异常。
    • 例:
      void test() throws Exception, RuntimeException{
          throw new Exception("测试异常");
      }
      
  • 可以用关键字 try 和 catch 捕捉异常。
    • 当 try 语句块出现一个异常时,会抛出该异常,依次与 catch 语句尝试匹配。
      • 如果该异常类型与某个 catch 语句声明的异常类型匹配,该异常就会被捕获,像给函数传递参数一样传递给该 catch 语句,执行其语句块。
      • 如果该异常不与任何 catch 语句匹配,则会抛出,供外层代码捕获。
      • 可以在 catch 语句之后定义一个 finally 语句块,无论是否出现异常都会执行它。
        • 即使 try、catch 中有执行 return 语句,系统也会先执行 finally 语句块再执行 return 语句。
      • 如果该异常始终没有被捕获,在 main 方法中抛出,就会导致程序终止,报错:unreported exception Exception; must be caught or declared to be thrown
    • 例:
      try{
          ...
      }catch(IOException i){    // IOException 表示异常类型,i 表示将异常实例化并传递给一个变量
          ...
      }catch(FileNotFoundException f)
          System.exit(1);       // 让程序以指定的返回码退出
      }finally{
          ...
      }