在后端开发的学习路线中,Java 是绕不开的核心语言。它继承了 C++ 的面向对象思想,同时简化了繁琐的语法与内存管理,更加简洁、安全、易于工程化。对于有 C++ 基础的同学而言,Java 很多语法逻辑、程序结构都高度相似,学习成本会大大降低。
本文从零开始系统梳理 JavaSE 核心基础知识,从环境结构、数据类型、方法与数组,到 JVM 内存模型、类与对象、继承多态、接口抽象,再到异常机制,覆盖入门与面试高频考点。全文结合 C++ 对比讲解,力求逻辑清晰、重点突出,既适合零基础快速入门,也可作为后端面试前的复习资料,希望能为大家的 Java 学习之路提供一份完整、扎实的参考。

1. 初识Java

如果说C++是对C的一系列提升的话,那么它主要是在对C功能上的提升,代价是更晦涩的语法,那么Java就可以认为是对C++在语法难度和功能上的优化。所以,很多C++的语法格式和思想在Java中依然通用,比如条件判断语句、循环语句等在C、C++、Java中都是相同的

1.1 JavaSE和JavaEE

JavaSE,即Java Standard edition,是Java的基础平台,用于开发桌面和简单的服务器程序,包含基本的语言特性与API

JavaEE,即Java Enterprise Edition,是JavaSE的扩展版本,使用大型、分布式企业应用和Web应用

1.2 体系结构中立

“Write once, Run Everywhere”,源自于Java编译器和JVM的相互配合。Java编译器将.java文件中的代码转换为.class文件中的字节码(就是二进制代码),随后把它交给JVM运行。JVM维护了一套自己的指令集,与平台和硬件无关。Java程序在执行时,Java解释器会逐条将字节码文件中的指令转换为CPU的指令集

1.3 初识Java的main方法

public class HelloWorld{
    public static void main(String[] args){
        System.out.println("hello world");
    }
}
  • 类包含在源文件中,方法包含在类中,语句包含在方法中
  • 一个源文件中只能有一个public类,main函数作为Java程序的执行入口,有着固定的声明方式,即:
public static void main(String[] args){}

1.3.1 Java程序的运行

我们的Java代码写在.java文件中,经由javac编译器生成.class字节码文件,它与平台无关,交给JVM使用;JVM会将字节码转换为平台能够理解的方式来执行

JDK、JRE、JVM三者的关系?

  • JVM,Java Virtual Machine,负责将javac生成的平台无关的字节码文件转换为平台能够理解的方式执行
  • JRE,Java Runtime Environment,是Java的运行时环境,包括了JVM、Java基础类库
  • JDK,Java Development Kit,是Java开发工具包,包含了JRE、javac以及自带的调试工具等

1.3.2 Java命名建议

  1. 类名大驼峰(HelloWorld)
  2. 方法名、变量名小驼峰(helloWorld)

2. 数据类型与变量

2.1 字面常量

常量即程序运行期间,固定不变的量,主要包含字符串常量、整形常量、浮点数常量、字符常量、布尔常量、空常量null

2.2 基本数据类型

包括四类八种

四类:整型、浮点型、字符型、布尔型

八种:字节型、短整型、整型、长整型、单精度浮点数、双精度浮点数、字符型、布尔型

以下几点需要注意

  • 整型无论在16位平台还是32位平台都是4个字节,长整型都是8个字节

  • 整形、浮点型默认都是带有符号的

  • 整形默认是int,浮点型默认是double

  • 字符串属于引用类型

2.3 变量

与C、C++声明、定义方式相同,但是需要注意的是,C++可以不给初始值使用默认值,Java在使用变量前必须初始化。

其次是关于浮点数,与C++一样,Integer相除默认是丢弃小数取整的,除非两个数中有一个浮点数(或者也可以对两个整型中的一个乘1.0),同时Java也和C++一样,采取有限空间表示无限的小数的方式,误差会存在的

还有就是关于字符型变量。在Java中不是采用的ASCII码来表示,而是UniCode来表示,因此字符型变量char是两个字节,也能够表示中文

最后是布尔型变量boolean,不同于C、C++,Java的布尔变量不能和整型之间相互转化,同时Java也没有对布尔型变量的大小作出规定

2.3 隐式、强制类型转换

隐式类型转换就是自己不用处理,编译时编译器会自动处理。数据范围小的转化为大的时自动进行(这样是安全的,不会丢失精度)

强制类型转换使用的方式和C、C++一样,在变量前加上(type)即可,不过需要注意的是,虽然这种方式可以在丢失精度的情况下将大的类型转化为小的类型(比如long->int),但是并不能将毫不相关的类型转换(比如char和boolean)

2.4 类型提升

2.4.1 int and long

int会被提升为long,即不同类型运算时,结果会提升为更大的类型

2.4.2 byte、short等

这些低于四个字节的类型,因为cpu一般以四个字节为单位从内存中取数据,所以会被提升为int

public class DataType{
    public static void main(String[] args){
        byte b1 = 1, b2 = 2;
        byte b3 = (byte)(b1 + b2); //类型提升与强制类型转换
    }
}

2.5 字符串类型

Java用String类来表示字符串类型。

可以通过String.valueOf(int)int + ""来完成整型转化为字符串,以及Integer.parseInt(String)来完成字符串转化成为整型

3. 标准输入输出

3.1 标准输出

主要有以下三种方法

System.out.println("hello world"); //打印一行文本,自动添加换行符
System.out.print("hello world"); //直接打印,不自动添加换行符
System.out.printf("hello %s", "world"); //print format

3.2 标准输入

需要导入包java.util.Scanneer

import java.util.Scanner;
public class InputOutput{
    public static void main(String[] args){
		Scanner sc = new Scanner(System.in);
         String str = sc.nextLine();
         int val = sc.nextInt();
         float f = sc.nextFloat();
    }
}

3.3 经典的猜数字游戏

需要导入包java.util.Random,初始化Random对象时直接指定范围

package JavaSE.InputOutput;
import java.util.Scanner;
import java.util.Random;

public class NumGuess{
    public static void main(String[] args){
        System.out.println("please guess a number between 1~100, input nothing to exit");
        Random rand = new Random();
        int answer = rand.nextInt(100);
        Scanner sc = new Scanner(System.in);
        while(sc.hasNextInt()){
            int guess = sc.nextInt();
            if(guess < answer){
                System.out.println("smaller than the answer! Guess again?");
            }
            else if(guess > answer){
                System.out.println("bigger than the answer! Guess again?");
            }
            else{
                System.out.println("correct");
                break;
            }
        }
        sc.close();
    }
}

4. 方法

4.1什么是方法

其实可以对应到C、C++中的函数,由修饰符 返回值 方法名([参数类型 参数名称...]){若干操作; return val;}的形式组成。不同的是,Java中函数首先,返回值不是void的话,必须明确返回值,不能省略return语句;其次方法不能写在类的外面,也不能嵌套定义,没有方法声明一说

4.2 方法重载

其实和C++的规则一样:返回值不同不构成函数重载,要求参数的类型、个数至少有一个满足不同(即调用时就能根据参数的情况唯一确定一个调用对象),同时函数名相同

4.3 方法签名

为什么能够在同一个类中定义相同的方法呢?这是因为在javac生成的字节码文件中,采取全路径+类型的方式来表示方法,具体如何表示可以使用javap这一自带的反汇编工具查看

在反汇编中,采取变量类型名称首字母大写来表示变量类型,例如int为I,double为D。但是Boolean为Z,数组以[开头,引用以L开头

5. 数组

5.1 初始化

Java中数组可以通过三种方式初始化:分别是动态初始化、静态初始化

int[] arr1 = new int[10]; //动态初始化
int[] arr2 = new int[]{1, 2, 3}; //静态初始化

对于动态初始化,开辟的JVM的堆空间用对应类型的0来作为默认值(boolean为false),静态初始化会根据指定元素的数量自动确定大小

Java依然允许通过C++的方式初始化数组,但是并不建议。

int arr3[] = {1, 2, 3};

对于静态初始化, 也可以采用这种省略的方式

int[] arr = {1, 2, 3};

同时,静态初始化和动态初始化也能够分离式的定义

int[] arr1;
arr1 = new int[10];
int[] arr2;
arr2 = new int[]{1, 2, 3};

但是省略写法的不可以

5.2 二维数组

int[][] arr = new int[3][3]; //要么动态初始化
int[][] arr = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; //要么静态初始化

对于二维数组来说,行数不可以省略,列数可以省略。对于函数省略的数组,我们把它叫做不规则的二维数组,可以通过以下方式指定每一行的大小

int[][] arr = new int[2][];
arr[0] = new int[3];
arr[1] = new int[4];

这样,我们就定义了一个两行,第一行三列,第二行四列的二维数组

5.3 关于引用

在这里插入图片描述

上面是JVM的结构(虚拟机栈在有的JVM实现中是合并在一起的)

1. 方法区(Method Area)

  • 属于线程共享区域
  • 存储内容:
    • 已加载的类信息(类名、访问修饰符、字段、方法描述等)
    • 运行时常量池(字面量、符号引用)
    • 静态变量(static)
    • JIT 即时编译器编译后的机器码
  • 字节码本身也在类加载后存放于此区域

2. 堆(Heap)

  • 线程共享,JVM 中最大的一块内存
  • 存放所有对象实例数组
  • 垃圾回收(GC)主要管理的区域
  • 字符串常量池在 JDK7 之后也从方法区移到了堆中

3. 虚拟机栈(JVM Stack)

  • 线程私有,生命周期与线程相同
  • 每个方法执行时,会创建一个栈帧(Stack Frame)
  • 栈帧中存放:
    • 局部变量表(基本数据类型、对象引用)
    • 操作数栈
    • 动态链接
    • 方法返回地址
  • 常说的 “栈内存” 一般就是指虚拟机栈

4. 本地方法栈(Native Method Stack)

  • 同样线程私有
  • 作用:为 native 方法(C/C++ 实现的本地方法)服务
  • 与虚拟机栈结构类似,只是服务对象不同
  • 很多 JVM 实现会将虚拟机栈 + 本地方法栈合并

5. 程序计数器(Program Counter Register)

  • 线程私有
  • 作用:记录当前线程下一条需要执行的字节码指令地址
  • 线程切换、恢复执行时依赖它

对于像数组这样的应用变量,在虚拟机栈中,他存储的就只是堆中存储的实际数据的地址,因此在传参、返回时不需要通过专门的应用类型来减少拷贝,因为本身相当于传的就是一个指针

在Java虚拟机中,与C++程序的进程地址空间,近似有以下的对应关系:

  • 方法区 ≈ 数据段 + 代码段:方法区主要存储类信息、字节码指令、编译后的机器指令、运行时常量池、字符串常量池,对应C++程序代码段存储编译好的机器码指令,以及代码段存放全局变量和静态变量的.bss/.data区与存放字符串常量和const常量的.rodata
  • 虚拟机栈 + 本地方法栈 = 栈
  • 堆 = 堆

6.类和对象

Java中,所有类(除了 Object 自己),都继承自 Object 类,其中有 toString 方法、equals 方法等定义好的方法。Object 类是参数的最高统一类型,所有对象都可以使用 Object 的引用进行接收

6.1构造函数

  • 和C++类似,构造函数编译器会生成一份默认的无参构造,并将成员变量都初始化为默认值;如果用户自己实现的话,要求函数名必须和类名相同,同时不能有返回值,void也不行。
  • 允许在声明成员变量时指定默认值,进行就地初始化,类似C++的方法,并指定修饰符

6.2关于this引用

  • 其实对应到了C++的this指针,用来指明访问的是那个对象的成员变量或者成员方法,谁调用,this引用就去引用谁。在对象内部访问本对象的成员变量、成员方法时,编译器都会加上这个隐式的this引用,也可以在成员方法中手动调用

  • 构造函数中,也可以用this引用去执行其他方式的构造,只要不构成循环调用即可,例如

public class Date{
    public int _year, _month, _day;
    public Date(){
        this(0, 0, 0);
    }
    public class Date(int year, int month, int day){
        _year = year; _month = month; _day = day;
    }
}

6.3对象的打印

可以通过重写toString方法实现

@Override
public String toString(){
    String str = "[" + _year + "-" + _month + "-" + _day + "]";
    return str;
}

6.4关于包:

,是Java对类、接口的封装机制的体现,而且不仅仅体现在类似于C++的命名空间上,更体现在文件路径、文件加上,即上升到了文件的层次,使得项目整体结构更加清晰

其实使用包的思想和使用C++的命名空间思路上是一样的,可以指定包中的类,比如Date类,这就类似于C++通过域作用限定符访问某个命名空间中的方法、成员

java.util.Date d = new java.util.Date();
System.out.println(d.getTime()); //后者毫秒级时间戳

第二种方法就是通过import导入,类似于C++的using namespace std这方式

import java.util.Date;
import java.lang.*;
Date d = new Date();

不过需要注意的是,可能会发生冲突,比如java.sql中也有Date

同时,还可以通过import static的方式导入包中的静态成员、静态方法

import static java.lang.Math.*;
System.out.println(sqrt(pow(2, 2.2) + pow(3, 3.3)));

6.4.1 static静态成员

  1. 和C++一样,static静态成员变量、成员函数不属于某一个具体类,而是所有该类的实例化对象共有的一份,可以通过类型名访问,也可以通过对象访问。静态成员变量的生命周期与类相同,从类的加载开始,从类的卸载结束。
  2. 关于静态成员的存储位置,前面提到过,Java的方法区中存储着类的信息,而静态成员作为类的一种属性,也存储在这里

6.4.2 static静态成员的初始化

就地构造
public class Info{
    private String name;
    private static String school = "Fudan University";
}

类似于C++中声明时给缺省值是一样的,直接使用给定的值就地构造

静态代码块初始化
public class info{
    static String classId;
    static String school;
    String name;
    String nickname;
    {
        name = "wangjiale";    //构造代码块
        nickname = "human";
    }
    static {
        classId = "1";    //静态代码块
        school = "Fudan University";
    }
    public String toString(){
        {
            String info = name + nickname + classId + school;    //普通代码块
        }
        return info;
	}
}

代码块主要分为四种:普通代码块、构造代码块、静态代码块、同步代码块

普通代码块,就是写在方法里的代码块

构造代码块,用来初始化类的成员变量

静态代码块,用来初始化类的静态成员变量

同步代码块,主要用在多进程、多线程中,后面再介绍

关于静态代码块,如果有多个的话,那么编译器会把这些静态代码块合并在一起,在整个程序的运行过程中,一个类的静态代码块指挥被执行以此;构造代码块只会在实例化类时执行

6.5 继承

6.5.1 extends

使用extends关键字来表示继承关系,不需要像C++一样指定继承方式,因为Java类的每个方法、属性都带有访问限定作用的修饰符

6.5.2 关于访问限定符的作用

访问权限 public protected default private
同一包中的同一类
同一包中的不同类
不同包中的子类
不同包中的非子类

对于类的方法和属性,只要没有加修饰符,都是default,在这个包内是公开的

6.5.3 super

使用super关键字来访问父类中同名的方法、变量,或者主动调用父类的构造方法完成父类的构造

继承体系中的构造顺序,和C++一样,都是采用先构造父类,再构造子类的方式。但是前面我们提到过,只要实现了自己的构造,编译器就不会再生成默认的无参构造。默认情况下,编译器为我们默认加上一个super()去调用父类的无参构造,在子类构造函数的第一句。但是如果编译器没有生成的话,就会报错。

所以这时我们需要手动调用父类的构造,比如以下情况

class Base{
    int a;
    public Base(int a){
        this.a = a;
    }
}
class Derive extends Base{
    public Derive(){
        super(0);
    }
}

thissuper

  1. this在非静态成员方法中,用来访问非静态的成员方法、变量;super在子类中,用来访问父类的成员方法、变量
  2. this可以用来在类的一种构造中调用另一种构造;super在子类的构造的第一个语句中,可以去调用父类的构造
  3. 编译器会自动在非静态的成员方法对非静态成员变量、方法的访问前加上this;无论父类的构造方法怎样,一定会去调用super(...)

6.5.4 继承关系

Java不允许多继承。C++允许多继承,虽然可以通过virtual虚继承的方式解决数据冗余和二义性的问题,但是这也有性能损耗,并且这种情况一般也可以通过设计避免。

Java直接杜绝了这种问题

final

作用于变量,起到C++中const的作用,表示其不可被修改

作用于类,和C++中一样,表示不可被继承

6.5.5 多态

  1. 多态,是在继承体系中,子类对父类的方法在保证方法名、参数列表不变的情况下,完成子类自己的方法体实现的行为。想要体现出多态,需要通过基类的引用去调用重写完成的方法.
  2. 对于多态的返回值,必须相同(除非能够构成父子关系)

Java的处理思路其实和C++类似:重写要求函数名和参数列表相同,返回值不同的话,必须是父类返回父类的指针和引用,子类返回子类的指针和引用,这被称作协变

  1. 关于访问权限问题:子类重写时,可以改变权限,但是可以缩小权限,比如从default到public
  2. 父类的private方法、静态方法都不能被重写
  3. 可以使用@Override来帮助检查是否完成重写,作用同C++的override

多态和重载的对比

多态 重载
参数列表 必须完全相同 必须不同,参数的个数、类型等
返回值 必须相同(除非构成父子关系) 都可以
访问限定符 可以改变,但是只能扩大允许访问的范围 扩大、缩小都可以

多态可以认为完成的是运行时的多态,是动态绑定,即运行时才能知道调用的是谁;重载完成的是编译时多态,是静态绑定,在编译时确定调用的方法

  1. 关于向上转型向下转型:两者可以认为是在自定义类这一级别上的类型转换规则。将子类对象赋值给父类,调用的都是父类的方法(或者子类重写的方法),这就是向上转型;将父类对象赋值给子类,调用的都是子类的方法,这就是向下转型

    但实际上,向下转换可能是不安全的。父类对象转换为子类对象,转换之后结果要求支持所有子类的访问接口,如果转换目标压根不满足父子关系或者父类对象使用其的其他父子关系向上转换来的,那么就会报错。

    Java为了提高安全性,引入了instanceof关键字来检查是否能够安全的向下转型

6.6 抽象类

  • 使用abstract修饰的类,是抽象类,不能实例化出对象
  • 类中如果有方法被abstract修饰,那么这个方法就是抽象方法(抽象方法不写函数体),这个类就是抽象类。即:抽象类不一定有抽象方法,但是有抽象方法的一定是抽象类
  • 抽象方法不能是private,因为抽象类的抽象方法必须被继承的子类重写了,才有意义;继承抽象类的子类,也只有重写了全部抽象函数,才能够实例化出对象,否则自己还是抽象类
  • 抽象类的适用场景,其实就是在一些情况中,有些方法不应该由父类去实现,应该由子类根据具体的需要做具体的处理。有了抽象类,我们就可以让编译器替我们做出这个检查,否则我们调用了父类的方法也不容易察觉

6.7 接口

6.7.1 What’s 接口

public interface IRun{
    void run();
}
  • 使用interface关键字来标识接口
  • 接口中的方法,名称建议使用大写I开头;所有的方法都会被默认指定为public abstract,也只能是public abstract,否则报错
  • 接口的使用,从上面默认的访问修饰,不难发现:接口和抽象类一样,必须完成了对其所有抽象方法的重写,用一个实现类来“实现”这些接口,才能够使用。类与接口之间,使用implements
public class 类名称 implements 接口名称1, 接口名称2...{
    //实现接口中所有的抽象方法
}
  • 接口和抽象类一样,不能直接实例化;重写接口中方法时,也不能使用默认的访问权限(实现类中默认是default,接口中默认是public,访问范围缩小了)
  • 接口中可以有变量,但是会被隐式的指定为public static final
  • 一个实现类可以实现多个接口,只用重写完所有接口的所有抽象方法即可

6.7.2 接口间的继承

Java中不允许类之间存在多继承的关系,但是允许接口之间存在多继承,使用extends关键字。通过这种方式,可以绕过来安全解决多继承数据冗余、二义性的问题

C++有一个问题,即多继承时会出现数据冗余、二义性的问题(不考虑虚继承)

Java的这种解决方式,就是为了解决这个问题。

对于普通类、抽象类,直接不允许多继承。对于接口的成员变量,因为都是默认的final public static,所以多继承体系中也只有一份;对于成员方法,因为实现类要自己重新全部实现才能使用,所以不存在调用混淆(虽然JDK8引入了default方法,但是当出现混淆时直接报错)

6.7.3 接口的使用:Comparable和Comparator、Clonable

其实类似于C++的比较运算符重载,为如何对两个类进行比较提供方法

1. 通过Comparable

class Student implements Comparable<Student>{
    public String name;
    public int score;
    public int compareTo(Student t){ //重写compareTo方法
        if(this.score < t.score){
            return -1;
        }
        else if(this.score > t.score){
            return 1;
        }
        else{
            return 0;
        }
    }
}

2. 通过Comparator

class NameComparator implements Comparator<Student>{
    public int compare(Student s1, Student s2){
        return s1.compareTo(s2);
    }
}

3. Clonable

Clonable,是Java中内置的创建对象拷贝的方法。但是这里的拷贝是浅拷贝(对于内置类型,是真正的拷贝;对于String这种不可修改类型,只是有了一个新的指向堆的引用,即表象上的深拷贝;对于其他自定义类型,会进行标准的浅拷贝,实际上用的

6.7.4 抽象类和内部类对比

区别 抽象类 接口
组成 普通方法+抽象方法 抽象方法+全局常量
权限 各种权限 public
子类使用 extends来继承抽象类 implements实现接口
子类限制 一个子类只能继承一个抽象类 一个接口可以实现多个接口
关系 一个抽象类可以实现多个接口 接口不能继承抽象类,允许接口的多继承

6.7.5 内部类

  1. 分类,实例内部类、静态内部类、局部内部类、内名内部类

  2. 注意事项

  • 实例内部类可以访问其外部类的所有非静态成员,外部类想要访问内部类的成员,就要先实例化内部类

  • 静态内部类可以访问其外部类的所有静态成员,想要访问非静态成员,就要先实例化外部类

  • 静态内部类的创建不需要先创建外部对象

  • 局部内部类:定义在外部类的方法体或者{}中,只能在定义的时候被使用

  • 匿名内部类:用于创建只使用一次的类,可以继承一个类后者实现一个方法,也和普通类一样,定义时不能直接写执行语句

7. 异常

Java 的异常抛出、捕获的机制和C++思路上几乎一致,都是按照函数调用栈往上寻找 catch 语句。只是具体语法细节、异常种类上有所不同

7.1 异常的体系结构

  1. Throwable,是异常体系的顶层类,派生出 Error 和 Exception两个子类
  2. Error,包括栈溢出、内存不足等 JVM 无法解决的问题
  3. Exception,可以由代码进行处理,可以不杀死程序,我们一般说的异常,就是指的 Exception
  4. 其他所有的异常,要么继承自 Error,要么继承自 Exception
  5. 所以类似于 C++ 的 const std::exception& e ,可以使用 Exception 来捕获所有异常

7.2 异常的分类

  1. 编译时异常,也称作受检查异常,即 javac 生成 .class 字节码文件这一阶段
  2. 运行时异常,也称作非受检查异常,包括空指针访问、数组越界等

7.3 异常处理

7.3.1 throw

抛出异常,指定一个异常对象即可。异常一旦抛出,后面的代码就不会执行,可以抛出Exception的子类,也可以是自定义的异常

7.3.2 throws

用来只是抛出的是哪个异常,跟在方法声明的参数列表之后

class getVal(int[] arr, int index) throws ArrayIndexOutOfBoundsException{
    //...
}

throws 并不是真的抛出异常的动作,而是将处理异常的任务交给调用者

7.3.3 try-catch finally

其实这里使用和C++几乎是一样的。不过 catch 可以一次捕获多个异常,例如

catch (ArrayIndexOutofBoundsException | NullPointerException e){
    //...
}

finally 是捕获最后的底牌,用处就像 C++ 的 catch(…),连它都没有的话,那么就会终止程序

7.3.4 查看调用栈信息

e.printStackTrace();

使用的目的类似 C++ 的 e.what(),不过这里展示的是函数调用栈以及异常信息

自定义Exception

  • 一般来说继承自 Exception 或者 RuntimeException,可以自定义对错误信息
  • 继承自 Exception 是受查异常,后者是非受查异常
class UserNameException extends Exception {
	public UserNameException(String message) {
		super(message);
	}
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐