Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程。Java具有简单性、面向对象、分布式健壮性安全性、平台独立与可移植性、多线程、动态性等特点。 Java可以编写桌面应用程序、Web应用程序、分布式系统嵌入式系统应用程序等。

原文链接:全网最完整Java学习笔记

基本数据类型

概述

Java的八大基本数据类型分别是:

  • 整型的byte、short、int、long;
  • 字符型的char;
  • 浮点型的float、double;
  • 布尔型的boolean。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Test {
public static void main(String[] args) {
// 1. 整型
byte byteData = 120;
short shortData = 30000;
int intData = 2000000000;
long longData = 9000000000000000000L;
// 2. 字符型
char charData = 'A';
// 3. 浮点型
float floatData = 3.14f;
double doubleData = 123.456;
// 4. 布尔型
boolean booleanData = true;
// 访问和输出变量值
System.out.println("整型:");
System.out.println("byteData: " + byteData);
System.out.println("shortData: " + shortData);
System.out.println("intData: " + intData);
System.out.println("longData: " + longData);
System.out.println("字符型:");
System.out.println("charData: " + charData);
System.out.println("浮点型:");
System.out.println("floatData: " + floatData);
System.out.println("doubleData: " + doubleData);
System.out.println("布尔型:");
System.out.println("booleanData: " + booleanData);
// 操作变量
intData++; // 增加 intData 的值
doubleData *= 2; // 将 doubleData 的值乘以 2
// 输出修改后的值
System.out.println("\n操作后的整型和浮点型变量:");
System.out.println("intData: " + intData);
System.out.println("doubleData: " + doubleData);
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
整型:
byteData: 120
shortData: 30000
intData: 2000000000
longData: 9000000000000000000
字符型:
charData: A
浮点型:
floatData: 3.14
doubleData: 123.456
布尔型:
booleanData: true
操作后的整型和浮点型变量:
intData: 2000000001
doubleData: 246.912

基本数据类型和引用类型

基本数据类型

基本数据类型共有八大类,这八大数据类型又可分为四小类,分别是整数类型(byte/short/int/long)、浮点类型(float、double)、字符类型(char)和布尔类型(boolean)。

引用类型

引用类型包括数组引用、类引用、接口引用,还有一种特殊的null类型,所谓引用数据类型就是对一个对象的引用,对象包括实例和数组两种。

区别

特征 基本数据类型 引用类型
存储方式 直接存储数据值 存储对象的引用,实际数据存储在堆内存中
默认值 有默认值,不可为null 有默认值为null
赋值方式 直接赋值 使用new关键字创建对象
内存分配 栈上分配 在堆上分配
大小 固定大小,与具体类型有关 大小不固定,由对象本身和其内容决定
效率 更高效,直接操作数据 相对较低,需要间接操作对象引用
比较 用==比较 通常使用equals方法比较
范围 有限,具体范围取决于数据类型 无限,取决于系统的内存大小
传递方式 值传递,传递的是实际的数据值 引用传递,传递的是对象的引用
示例 int num = 42; String str = new String("Hello");
JVM存储位置 方法参数和局部变量:存在本地方法栈的局部变量表;final常量、静态变量:存在类常量池

基本数据类型的内存空间

对于基本数据类型,你需要了解每种类型所占据的内存空间,这是面试官喜欢追问的问题:

  • byte:1字节(8位),数据范围是 -2^7 ~ 2^7-1
  • short:2字节(16位),数据范围是 -2^15 ~ 2^15-1
  • int:4字节(32位),数据范围是 -2^31 ~ 2^31-1
  • long:8字节(64位),数据范围是 -2^63 ~ 2^63-1。c语言里long占4字节,long long占8字节。
  • float:4字节(32位),数据范围大约是 -3.4*10^38 ~ 3.4*10^38
  • double:8字节(64位),数据范围大约是 -1.8*10^308 ~ 1.8*10^308
  • char:2字节(16位),数据范围是 \u0000 ~ \uffffunicode编码英文和中文都占两个字节。C语言使用ASCII编码char占1字节,不能存汉字。ASCII编码是Unicode的一个子集,因此它们存在一些字符码值是相等的。
  • boolean:Java规范没有明确的规定,不同的JVM有不同的实现机制。

数组

创建

数组的两种创建方式

一维数组:数组命名有两种方式。

1
2
3
4
// 方式一(推荐,符合阿里规约):a和b都是一维数组 
int[] a,b;
// 方式二:c和d都是一维数组,不推荐这种命名方法
int c[],d[];

二维数组

1
int[][] x;

阿里规约:【强制】 类型与中括号紧挨相连来表示数组。

  1. 正例: 定义整形数组 int[] arrayDemo;

  2. 反例: 在 main 参数中,使用 String args[]来定义

数组的创建原理

JVM内存模型什么是JVM的内存模型?详细阐述Java中局部变量、常量、类名等信息在JVM中的存储位置

数组存放在JVM的堆中,是一片连续的存储空间,下标依次为0,1,2,3,4…

数组在JVM中的创建过程

  1. main方法进入方法栈执行
  2. 创建数组,JVM会在堆内存中开辟空间,存储数组
  3. 数组在内存中会有自己的内存地址,以十六进制数表示
  4. 数组中有3个元素,默认值为0
  5. JVM将数组的内存地址赋值给引用类型变量array
  6. 变量array保存的是数组内存中的地址,而不是一个具体数值,因此称为引用数据类型。

初始化

Java数组初始化有两种方式,分别是静态初始化、动态初始化。

  1. 静态初始化:不指定数组长度,由JVM自己识别

  2. 动态初始化:指定数组长度

静态初始化

静态初始化:不指定数组长度,由JVM自己识别

格式:数据类型[] 数组名=new 数据类型 {元素1,元素2,元素3…};

1
2
3
4
5
// 静态初始化:不指定长度,由系统猜
// 简写格式(推荐)
int[] c = {1,2,3};
// 完整格式
int[] ff = new int[]{1,2,3};

动态初始化

动态初始化:指定数组长度

格式:数据类型[] 数组名=new 数据类型[数组的长度];

1
2
// 动态初始化:指定长度
int[] ff = new int[3];

访问

基本访问方式

通过下标访问数组元素:

数组存放在JVM的堆中,是一片连续的存储空间,下标依次为0,1,2,3,4…

使用数组的索引(下标)来访问特定位置的元素。数组的索引从 0 开始,一直到数组长度减一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int[] numbers = {1, 2, 3, 4, 5};
// 访问索引为 2 的元素,即数组中的第三个元素
int element = numbers[2];
System.out.println(element); // 输出:3

// 使用循环结构(如 for 或 foreach)遍历数组,访问每个元素
// 使用 for 循环
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
// 使用 foreach 循环
for (int num : numbers) {
System.out.println(num);
}

高级访问方式:迭代器、Stream流、toString

遍历:迭代器

对于集合类(如 ArrayList),可以使用迭代器来访问元素。

1
2
3
4
5
6
7
8
9
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int element = iterator.next();
System.out.println(element);
}

遍历:Java 8 的 Stream

使用 Java 8 引入的 Stream API 进行数组遍历和操作。

1
2
3
4
int[] numbers = {1, 2, 3, 4, 5};

// 使用 Stream.forEach
Arrays.stream(numbers).forEach(System.out::println);

打印全部:Arrays 类的 toString 方法

使用 Arrays 类的 toString 方法将整个数组转换为字符串。

1
2
3
4
5
int[] numbers = {1, 2, 3, 4, 5};
// 输出:[1, 2, 3, 4, 5]
System.out.println(Arrays.toString(numbers));
// 直接输出数组会输出地址:[I@433c675d
System.out.println(numbers);

操作

数组本身的方法很少,只有equals()、stream()等简单方法,一些对数组的高级操作,可将数组转为String或集合,操作后再转回数组。

数组和字符串的互相转换

1
2
3
4
5
6
7
8
// 数组转字符串:Arrays.toString(数组名)
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(Arrays.toString(numbers));
// 字符串转数组: stringToIntArray(String str)、stringToCharArray(String str)等
// 字符串转整型数组
int[] intArray = stringToIntArray("1 2 3 4 5");
// 字符串转字符数组
char[] charArray = stringToCharArray("Hello");

数组和集合的互相转换

  1. 数组转List:List arrayToList(T[] array)
  2. List转数组:T[] listToArray(List list, Class elementType)
  3. 数组转Set:Set arrayToSet(T[] array)
  4. Set转数组:T[] setToArray(Set set, Class elementType)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 数组转List
String[] array = {"apple", "banana", "orange"};
List<String> listFromArray = Arrays.asList(array);
System.out.println("List from Array: " + listFromArray);
// 2. List转数组
List<String> fruitList = new ArrayList<>(List.of("apple", "banana", "orange"));
String[] arrayFromList = fruitList.toArray(new String[0]);
System.out.println("Array from List: " + Arrays.toString(arrayFromList));
// 3. List转Set
Set<String> setFromList = new HashSet<>(fruitList);
System.out.println("Set from List: " + setFromList);
// 4. Set转List
Set<String> fruitSet = new HashSet<>(Set.of("apple", "banana", "orange"));
List<String> listFromSet = new ArrayList<>(fruitSet);
System.out.println("List from Set: " + listFromSet);
// 5. Set转数组
String[] arrayFromSet = fruitSet.toArray(new String[0]);
System.out.println("Array from Set: " + Arrays.toString(arrayFromSet));
// 6. 数组转Set
Set<String> setFromArray = new HashSet<>(Arrays.asList(array));
System.out.println("Set from Array: " + setFromArray);

// 转为集合后,可以通过集合的api操作各元素。例如:判断数组是否包含某个元素:
int[] array = {1, 2, 3, 4, 5};
boolean isEle = Arrays.asList(array).contains("a");
System.out.println(isEle);

流程控制语句

概述

流程控制语句分类:

  • 顺序结构
  • 分支结构(if、switch)
  • 循环结构(for、while、do…while)

IF分支语句

1
2
3
4
5
6
7
if (关系表达式1) {
语句体1;
} else if (关系表达式2) {
语句体2;
} else {
语句体n+1;
}

执行流程:

  1. 首先计算关系表达式1的值
  2. 如果值为true就执行语句体1;如果值为false就计算关系表达式2的值
  3. 如果值为true就执行语句体2;如果值为false就计算关系表达式3的值 …
  4. 如果没有任何关系表达式值为true就执行语句体n+1

switch分支语句

1
2
3
4
5
6
7
8
9
10
11
12
switch (表达式) {
case1:
语句体1;
break;
case2:
语句体2;
break;
// ...
default:
语句体n+1;
break; //最后一个可以省略
}

格式说明

  1. 表达式:取值为byte、short、int、char,JDK5以后可以是枚举,JDK7以后可以是String

  2. case:后面跟的是要跟表达式比较的值

  3. break:表示中断结束的意思,用来结束switch语句

  4. default:表示所有情况都不匹配的时候,就执行该处内容,和if语句中的else相似

执行流程

  1. 首先计算表达式的值

  2. 以此和case后面的值进行比较,如果有对应值,就会执行相应语句,在执行过程中,遇到break就会结束

  3. 如果所有的case的值和表达式的值都不匹配,就会执行default里面的语句体,然后程序结束

注意事项:在switch语句中,如果case控制的语句后面不写break,将会出现”穿透”现象。


修饰符

访问权限修饰符

  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • protected : 对同包可见、对不同包子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
  • default : 同包可见。使用对象:类、接口、变量、方法。
  • private: 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
修饰符 当前类 同一包内 子孙类(同一包) 子孙类(不同包) 其他包
public Y Y Y Y Y
protected Y Y Y Y/N(说明 N
default Y Y Y N N
private Y N N N N

private

在同一类内可见,保护成员不被别的类使用。可以修饰变量、方法。 注意:不能修饰类(外部类)

private变量不能被其他类直接访问,但可以通过get和set方法间接访问:

  1. get变量名()方法:获取成员变量的值,方法用public修饰
  2. set变量名(参数)方法:设置成员变量的值,方法用public修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Phone {
int price; // 成员变量在堆内存
private int age;

public void setAge(int a) {
age = a;
}
// 或者用this
// public void setAge(int age) {
// this.age = age;
// }
public int getAge() {
return age;
}
}
// 使用
public class hello {
public static void main(String args[]) {
Phone p = new Phone();
p.setAge(12);
System.out.println(p.getAge());

}

}

static

基本介绍

静态成员变量被所有对象共享,可用类名调用。局部变量不能被声明为 static 变量。

1
2
3
4
5
6
7
8
9
10
11
public class Phone {
static int price; // 成员变量在堆内存
}
// 使用
public class hello {
public static void main(String args[]) {
Phone.price=4;
//fun();会报错,静态方法只能访问静态方法或变量。
}
public fun(){};
}

静态方法只能访问静态变量和方法。非静态方法都可以访问。

静态方法中不能使用 this 关键字,因为在静态方法的局部变量表中并不存在this变量。

类变量

static可以修饰什么?

Java类中包含了成员变量、方法、构造器、初始化块和内部类(包括接口、枚举)5种成员。

static关键字可以修饰成员变量、方法、初始化块和内部类,不能修饰构造器

static访问规则

被static修饰的成员先于对象存在,所以又称为类变量。

类成员不能访问实例成员,即静态不能访问非静态,静态中也没有this关键字。this是随着对象的创建存在的。

类加载过程中类变量是怎样创建的?

在类加载过程中的链接-准备阶段,JVM会给类变量赋零值,初始化阶段为类变量赋初值,执行静态代码块。

类加载过程:加载、链接(验证、准备、解析)、初始化。这个过程是在类加载子系统完成的。

加载:生成类的Class对象。

  1. 通过一个类的全限定名获取定义此类的二进制字节流(即编译时生成的类的class字节码文件)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。包括创建运行时常量池,将类常量池的部分符号引用放入运行时常量池。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。注意类的class对象是运行时生成的,类的class字节码文件是编译时生成的。

链接:将类的二进制数据合并到JRE中。该过程分为以下3个阶段:

  • 验证

    确保代码符合JAVA虚拟机规范和安全约束。包括文件格式验证、元数据验证、字节码验证、符号引用验证。

    • 文件格式验证:验证字节码文件是否符合规范。

      • 魔数:是否魔数0xCAFEBABE开头

      • 版本号:版本号是否在JVM兼容范围

        常量类型:类常量池里常量类型是否合法

        索引值:索引值是否指向不存在或不符合类型的常量。

    • 元数据验证:元数据是字节码里类的全名、方法信息、字段信息、继承关系等。

      • 标识符:验证类名接口名标识符有没有符合规范
      • 接口实现方法:有没有实现接口的所有方法
      • 抽象类实现方法:有没有实现抽象类的所有抽象方法
      • final类:是不是继承了final类。
    • 指令验证:主要校验类的方法体,通过数据流和控制流分析,保证方法在运行时不会危害虚拟机安全。

      • 类型转换:保证方法体中的类型转换是否有效。例如把某个类强转成没继承关系的类
      • 跳转指令:保证跳转指令不会跳转到方法体以外的字节码指令上;
      • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
    • 符号引用验证:确保后面解析阶段能正常执行。

      • 类全限定名地址:验证类全限定名是否能找到对应的类字节码文件
      • 引用地址:引用指向地址是否存在实例
      • 引用权限:是否有权引用
  • 准备:为类变量(即static变量)分配内存并赋零值。

  • 解析:将方法区-运行时常量池内的符号引用(类的名字、成员名、标识符)转为直接引用(实际内存地址,不包含任何抽象信息,因此可以直接使用)。

初始化:类变量赋初值、执行静态语句块。

abstract

基本介绍

一个类不能同时被 abstract 和 final 修饰,抽象类的唯一目的是为了将来对该类进行扩充。
抽象类可以包括抽象方法和非抽象方法,抽象方法只能存在于抽象类中。
非抽象子类必须重写抽象父类中的所有抽象方法,抽象子类可以直接继承。

接口和抽象类的区别

设计目的

  • 接口作为系统与外界交互的窗口,体现了一种规范。它只能定义抽象方法及常量,而不允许存在初始化块、构造器、成员变量。
  • 抽象类是系统中多个子类的共同父类,它体现了一种模板式设计,可被当作系统实现过程中的中间产品,必须要有更进一步的完善。

相同点

  • 实例化:接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其它类实现和继承
  • 抽象方法:接口和抽象类都可以有抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法
  • 静态方法:在JDK8引入接口静态方法后,接口和抽象类都可以有静态方法。

不同点

  • 普通方法:接口里只能包含抽象方法和默认方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法。
  • 普通成员变量:接口里只能定义静态常量(会自动加static final,final常量必须显示的指定初始值),不能定义普通成员变量;抽象类里既可以定义普通成员变量,也可以定义静态常量
  • 构造器:接口里不包含构造器;抽象类可以包含构造器,但抽象类的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
  • 初始化块:接口里不能包含初始化块,抽象类则可以包含初始化块(静态代码块和实例代码块)
  • 单继承多实现:一个类最多能有一个父类,包括抽象类;但一个类可直接实现多个接口,通过实现多个接口可弥补Java单继承的不足

final

  1. final 变量必须显式指定初始值,不能被重新赋值(非final成员变量都会自动有默认值)。
  2. final方法不能被子类重写。
  3. final类不能被继承。
  4. final引用不能变地址值,可以变地址内容。如final M m=new M();m.age=12;是正确,引用m始终指向对象M的内存地址。

常用关键字

this

this指代当前对象的引用。通过this可以获取当前对象中的成员变量、方法。常可以用于方法的形参与成员变量同名时进行区分:

1
2
3
4
5
6
7
8
9
10
11
public class Phone {
private int age;
// 如果这个方法是static的,则会报错。
// 因为this指向当前对象的引用,不是指向当前类的引用。static方法是类方法,类方法不创建对象就可以通过类名调用。
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}

this指代构造方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Dog {
private int age;
public Dog() {
System.out.println("无参构造方法。。");
}
public Dog(int age) {
//先执行一次带参构造方法
this();
System.out.println("带参构造方法,年龄:"+age);
this.age = age;
}
public static void main(String[] args) {
//带参构造方法创建对象
Dog dog = new Dog(23);
}
}

super

指代仅包括父类特征的当前对象的引用。可以看做是父类的引用。

通过super可以获取当前对象的父对象的成员变量、方法。

以age变量为例,父类的age变量是3,子类的age变量是4,子类方法的age局部变量是5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal {
public String name;
int age = 3;
public void eat() {
// 吃东西方法的具体实现
System.out.println("吃东西");
}
}
// 子类
public class Dog extends Animal {
int age = 4;
public void show(){
int age = 5;
// 输出5 访问上一行的局部变量
System.out.println(age);
// 输出4 通过this访问当前对象的成员变量age
System.out.println(this.age);
// 输出3 通过super访问当前对象的父对象的成员变量age
System.out.println(super.age);
}
}

指代父类构造方法的调用

通过super(),可以调用父类的各个构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Animal {
public String name;
int age = 3;
public void eat() {
// 吃东西方法的具体实现
System.out.println("吃东西");
}
public Animal(String name) {
System.out.println("动物类带参构造方法"+name);
this.name = name;
}
public void sleep() {
// 睡觉方法的具体实现
}
}
public class Dog extends Animal {
public Dog(int age,String name) {
// 调用父类的带参构造方法
super("小白");
System.out.println("带参构造方法"+age+","+name);
}

public static void main(String[] args) {
new Dog(23,"小黑");
}
}

this和super的区别

this和super的区别:

  • this 是当前对象的引用,super 是当前对象的父对象的引用。
  • this()是构造方法中调用本类其他的构造方法,super()是当前对象构造方法中去调用自己父类的构造方法。
  • 静态中没有this和super关键字。this指向当前实例,super指向父类实例,是随着对象的创建存在的。

注意:子类所有无参、带参构造方法第一行都会隐式或显式地加super()。this()和super()方法不能显式的共存,但可以隐式的共存,且都只能显式地出现在构造方法的第一行。

  • 如果都不加,则系统会隐式加super();
  • 如果加super()或super(带参),系统不会再隐式加super();
  • 如果加this(),则系统会隐式在this()前加super();