开始

项目里面放模块,模块里放包。
main方法快捷键 psvm+enter:

1
2
public static void main(String[] args) { 
}

控制台原样输出 sout+enter:

1
System.out.println();

ctrl+y:删除一行
ctrl+d:复制一行
注释:单行//,多行/*+enter 文档注释

无论主方法main在哪个类里,只要文件中存在public类,文件名就由那个类决定

Java概述

诞生:Java是1995年6月由Sun公司引进到我们这个世界的革命性的编程语言。1990年Sun公司成立了由James Gosling领导的开发小组,开始致力于开发一种可移植的、跨平台的语言Java。

Java,一门很好的面向对象语言,其平台无关性让Java成为编写网络应用程序的佼佼者,而且Java也提供了许多以网络应用为核心的技术,使得Java特别适合于网络应用软件的设计与开发。

Java特点:简单、面向对象、平台无关、多线程、动态。

  1. 简单:C++需要手动管理内存,而Java采用自动垃圾回收机制。
  2. 面向对象:通过封装数据和行为、利用继承建立类的层次结构、借助多态实现灵活性以及运用抽象简化复杂性,来组织代码和解决问题。
  3. 平台无关:平台由操作系统OS和处理器CPU所构成。平台无关就是指软件的运行不因操作系统、处理器的变化而无法运行或出现运行错误。
  4. 动态:Java程序的基本组成单元是类(一切代码都必须定义在类中),有些类是自己编写的,有一些是从类库中引入的,而类又是运行时动态装载的,这就使得Java可以在分布环境中动态地维护程序及类库。

每个平台都会形成自己独特的机器指令(由0、1组成的序列代码)相同的CPU和不同的操作系统所形成的平台的机器指令可能是不同的。

  • 为什么平台无关?
    Java可以在平台之上再提供一个Java运行环境,该运行环境由Java虚拟机、类库以及一些核心文件组成。Java虚拟机核心可以直接识别字节码指令(由0、1组成的序列代码)Java针对不同平台提供的Java虚拟机的字节码指令都是相同的。所以要注意字节码≠机器指令

Java三大特性:封装、继承、多态。

拓展:javac(Java Compiler)是JDK提供的编译器,它的作用是后缀名为.java的源代码文件编译成后缀名为.class的节码文件

数据类型

标识符

在Java中,标识符是用来命名各种程序元素的有效字符序列。简单来说,标识符就是这些元素的名字。

命名注意:Java是区分大小写的语言,标识符的第一个字符不能是数字。

小驼峰命名法、大驼峰命名法、全大写命名法(如MAX_VALUE、DATABASE_URL)。

基本数据类型

Java语言中有8种基本数据类型:boolean、byte、short、int、long、float、double、char

8种基本数据类型习惯上可分为以下四大类型:

  1. 逻辑类型:boolean
  2. 整数类型:byte、int、short、long
  3. 字符类型:char
  4. 浮点类型:float、double

整数类型

int型常量共有4种表示方法

  1. 十进制: 123,6000
  2. 八进制: 077( 数字零做前缀 )
  3. 十六进制: 0x3ABC( 0x或 0X做前缀 )
  4. 二进制 :0b111( 用0b或0B做前缀 )

    对于int型变量,内存分配给4个字节(byte),占32位。(取值范围是-2**31 ~ 2**31-1

对于byte型内存分配给1个字节,占8位 .(−𝟐**𝟕 ~ 𝟐**𝟕 − 𝟏, 取值范围是从 -128 到 127)

对于short型变量,内存分配给2个字节,占16位.

对于long型变量,内存分配给8个字节,占64位。

字符类型

对于char型变量,内存分配给2个字节,占16位

转义符号的使用:\n(换行),\b(退格),\t(水平制表)

获取Unicode码点值,使用int型显示转换。
如果要得到一个0~65536之间的数所代表的Unicode表中相应位置上的字符,必须使用char型显示转换。

1
2
int position = (int) 'a';//直接转换并打印'a'的Unicode码点值97
char ch = (char)97;//'a'

浮点类型

float类型(单精度型)
float变量在存储时保留8位有效数字。

对于float型变量,内存分配给4个字节,占32位。

float常量后面必须要有后缀f或F

1
float x=22.76f, tom=1234.987f, weight=1e-12F;

double类型(双精度型)
double变量在存储double型数据时保留16位有效数字。
double常量后缀有“d”或“D” ,但允许省略后缀

  • 一个具有小数部分的数据的默认类型是double而不是float,为什么?
    因为float常量后面必须要有后缀“f”或“F”。

类型转换

Java中数据的基本类型(不包括逻辑类型)按精度从“低”到“高”排列:
byte -> short ->char -> int -> long -> float -> double

低->高:系统自动完成转换。
高->低:必须自己使用类型转换运算

  • 当把一个int型常量赋值给一个byte和short型变量时(判断:高->低),不可以超出这些变量的取值范围,否则必须进行类型转换运算
1
2
3
4
5
byte b = (byte) 128; //(√)

float x = 12.4 ; //(×)
float x = (float) 12.4 ; //(√)
float x = 12.4f ;//(√)

输入输出

1
Scanner reader=new Scanner(System.in);

reader对象用空白做分隔标记,读取当前程序的键盘缓冲区中的“单词”。

1
2
3
4
5
6
7
8
9
10
import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);//创建对象
System.out.print("请输入一个整数: ");//系统提示输入
int num = scanner.nextInt();//获取用户输入
System.out.println("你输入的是: " + num);//系统打印
}
}

System.out.println()System.out.print()的区别是前者输出数据后换行,后者不换行。

格式控制符:
%d:输出int类型数据值
%c:输出char型数据。
%f:输出浮点型数据,小数部分最多保留6位
%s:输出字符串数据。

数组

数组是相同类型的数据按顺序组成的一种复合数据类型。

数组属于引用型变量,数组变量中存放着数组首元素的内存地址

数组声明:

1
2
3
4
5
6
//一维
float boy[ ];
char [ ] cat;
//二维
float a[ ][ ];
Char [ ][ ] b;

为数组分配元素:

1
float boy[] = new float[4];

在声明数组的同时也可以给数组的元素一个初始值,如:float boy[] = { 21.3f, 23.89f, 2.0f, 23f};

int a[20];”是正确的数组声明。(x)
Java里面的声明于创建/实例化是两个独立的步骤。

数组长度查询
对于一维数组,float boy[]=new float[4],boy.length = 4.
对于二维数组,int [][] a = new int[3][8],a.length = 3含有的一维数组的个数 ).a[0].length = 8

因为数组是引用类型,所以不同的变量可以指向同一个数组对象。改变其中一个变量所指向的数组内容会影响所有指向该数组的对象

运算符和语句

运算符

java中数据类型的精度从“低”到“高”排列的顺序是:
byte -> short ~ char -> int -> long -> float -> double

  1. 表达式按最高精度进行运算。
  2. 如果表达式中最高精度低于int型整数,则按int精度进行运算。

赋值运算符“=
等号逻辑运算符“==

instanceof 运算符:是二目运算符,左面的操作元是一个对象,右面是一个类。
当左面的对象是右面的类或子类创建的对象时,该运算符运算的结果是true ,否则是false。

语句

break语句,那么整个循环语句就结束。
continue语句,那么本次循环就结束。

for循环语句:

1
2
3
4
5
6
7
for(int n=0; n<b.length; n++) { //传统方式
System.out.println(b[n]);
}

for(char ch:b) { //循环变量ch依次取数组b的每一个元素的值(改进方式)
System.out.println(ch);
}

类声明的变量被称作对象。类是用来创建对象的模板。
写类的目的:为了描述一类事物共有的属性和功能。
如果类名使用拉丁字母,那么名字的首字母使用大写字母。

类体的内容有两部分:一部分是变量的声明,用来刻画属性;另一部分是方法的定义,用来刻画行为功能。

声明成员变量时如果没有指定初始值,Java编译器会为其指定默认值。(boolean默认false,整数类型默认0,char变量默认空字符……)

区分成员变量与局部变量:

  1. 如果局部变量的名字与成员变量的名字相同,则成员变量被隐藏。(如果想在该方法中使用被隐藏的成员变量,必须使用关键字this。)
  2. 成员变量有默认值,但局部变量没有默认值,也没有访问修饰符

局部变量可以用final修饰符,注意它不是访问修饰符,易搞混。

➢类是体现了Java封装性的一种数据类型,封装了数据和对数据的操作。

注意:Java 中,类体中只能定义成员变量、成员方法、构造方法等,不能直接写执行语句(如赋值语句、打印语句等)—— 执行语句必须放在「方法、构造方法或代码块」中。

关于主类main:

元素 访问权限 说明
main方法 必须public 为了让JVM(它位于任何包之外)能够顺利地访问并执行这个方法,该main方法必须被声明为public
main方法所在的类 默认或者public 只需具备包访问权限即可被JVM加载和执行。
                          |

UML图

UML(Unified Modeling Language,统一建模语言)

类的UML图,使用一个长方形描述一个类的主要构成,将长方形垂直地分为三层。
类的UML图

  1. 如果A类中的成员变量是B类声明的对象,那么A类的对象关联于B类的对象(或 A类的对象组合了B类的对象)。
    类的UML图:关联
  2. 如果A类中某个方法的参数是用B类声明的对象或某个方法返回的数据类型是B类对象,A依赖于B。
    类的UML图:依赖
  3. 继承关系的类UML图,实线的起始端是子类的UML图,终点端是父类的UML图,但终点端使用一个空心的三角形表示实线的结束。
    类的UML图:继承

创建对象

类声明的变量被称作对象。创建对象(也称给出了这个类的一个实例)包括对象的声明为对象分配变量两个步骤。

空对象不能使用,因为他还没有得到任何的实体,必须进行对象分配变量的操作,即对象分配实体。

用new运算符给类的成员变量分配内存空间。如果成员变量在声明时没有指定初值,则使用默认值。各成员变量是属于该对象的实体。
然后计算出一个引用值(包含成员变量内存位置及相关信息)。例如new XiyoujiRenwu() 是一个引用值。

问:用new运算符和构造方法创建对象的正确步骤到底是什么?
1.成员变量分配内存空间,并指定默认值。
2.初始化成员变量,即用户声明成员变量时给定的默认值。
3.执行构造方法。
4.计算出一个引用值。

对象的组合:一个类可以把某个对象作为自己的一个成员变量。注意组合的时候不要用new!

1
2
3
class A{
B b;
}

如果一个对象a组合了对象b,那么对象a就可以委托对象b调用b的方法,即对象a以组合的方式复用对象b的方法

构造方法

构造方法的作用:在创建对象时使用,主要是用来初始化各个成员变量
构造方法的名字必须与它所在的类的名字完全相同,而且没有类型。

◆允许一个类中编写多个构造方法,但必须保证他们的参数不同,即参数的个数不同,或者是参数的类型不同。
◆如果类中没有编写构造方法,系统会默认该类只有一个构造方法,该默认的构造方法是无参数的,且方法体中没有语句。
◆如果类里定义了一个或多个构造方法,那么Java不提供默认的构造方法 。

参数传值

当对象调用方法时,参数被分配内存空间:当调用一个方法并传递参数给它时,这些参数会在方法的作用域内被分配内存空间

◆对于基本数据类型,Java采用的是值传递,方法接收的是实际值的一个副本方法内对参数的任何修改都不会影响原始变量

1
2
3
4
5
6
7
8
9
public static void main(String[] args){
int x = 5;
changeValue(x);
System.out.println("After method call, x = ", x);//输出After method call, x = 5

public static void changeValue(int a){
a = 10;
}
}

◆对于对象引用,传递的是引用的一个副本。方法不能直接修改传递进来的引用本身(比如让其指向另一个对象),但它可以修改该引用所指向对象的状态

运行结果:After method call, b = Hello World!

传值机制的意义:确保了方法只能访问它自己的副本,从而保护了外部代码的数据完整性,同时也允许方法有效地操作复杂的数据结构。

关键字static

在声明成员变量时,用关键字static给予修饰的称作类变量(也称静态变量),否则称作实例变量

二者区别?
➢ 不同对象的实例变量互不相同(不同的内存空间)
所有对象共享类变量(相同的一处内存),通过类名直接访问类变量 (类名.类变量名)

类似地,类中的方法也可分为实例方法和类方法(或静态方法)。

但是,和实例方法不同的是,类方法不可以操作实例变量,为什么?
➢因为在类创建对象之前,实例成员变量还没有分配内存。

其实也可以访问,但是必须显式传入该对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
pulic class Student{
private String name;

public Student(String name){
this.name = name;
}

public static void printName(Student s){
if(s != null){
System.out.println(s.name);//通过创建的对象去访问实例变量
}
}
}

类方法不属于任何对象,它属于“类”本身。用对象调用会让人误以为这个方法和“这个对象的状态”有关,但实际上它和对象毫无关系。

1
2
3
4
5
6
7
8
9
10
11
12
public class MathUtils{
public static int add(int a, int b){
return a + b;
}
}

//最标准、最清晰的方式
int result = MathUtils.add(3, 5);

//语法上允许,但容易让人误解
MathUtils = new MathUtils();
int result = utils.add(3, 5);

this不可以出现在static方法中,因为this表示当前对象

方法重载

Java中存在两种多态:重载(Overload)和重写(Override)。

方法重载:允许在同一个类中定义多个方法名相同但参数列表不同(参数个数不同 、参数类型相同但是类型不同)的方法。编译器根据调用时传递的参数类型和数量来决定具体调用哪个方法。

关键字this

this是Java的一个关键字,表示某个对象。this可以出现在实例方法和构造方法中,但不可以出现在static方法中,因为this表示当前对象

主要用途:

  1. 引用当前对象实例。
  2. 区分成员变量和局部变量:当局部变量与成员变量同名时,this可以用来明确被隐藏的成员变量。
  3. 调用另一个构造方法:使用this语法可以在一个构造方法中调用该类的另一个构造方法。

包名的目的是有效地区分名字相同的类。不同Java源文件中两个类名字相同时,它们可以通过隶属不同的包来相互区分。

包声明:`package com.example.app;

如果不写 package 声明,java 文件放在哪个物理文件夹,它就在哪个文件夹里。但 Java 不认为它‘属于某个命名包’,没有“包名”,所以不能用import,也不能被其他包访问!唯一能访问 Test 的,是和它在同一个默认包下的其他类。

import

import java.until.Date; 表示引入java.util包中的Date类 。

import java.util.*;表示引入java.util包中所有的类。

访问权限


局部变量没有访问修饰符。

如果一个类的所有构造方法的访问权限都是private的,意味着这个类不能有子类,理由是: 一个类的private方法不能在其他类中被使用,但子类的构造方法中一定会调用父类的某个构造方法。

继承

子类与父类

Java不支持多重继承(子类只能有一个父类)。Java的类按继承关系形成树形结构。这个树形结构中,根节点是Object类(Object是java.lang包中的类),即Object是所有类的祖先类。

如果一个类(除了Object类)的声明中没有使用extends关键字,这个类被系统默认为是Object的子类,即类声明“classA”与“class A extends Object”是等同的。

继承与多态:这里的多态性就是指父类的某个方法被其子类重写时,可以各自产生自己的功能行为。

子类的继承性

如果子类和父类在同一个包中,那么,子类自然地继承其父类中不是private的成员变量作为自己的成员变量和方法。继承的成员变量或方法的访问权限保持不变。

如果子类和父类不在同一个包中,那么,子类继承父类的protectedpublic成员变量和方法

子类与对象

用子类的构造方法创建一个子类的对象时,不仅子类中声明的成员变量被分配了内存空间,而且父类的成员变量也都分配了内存空间,但只将其中一部分作为分配给子类对象的变量(子类继承的那部分成员变量)。

父类中的private变量不作为子类对象的变量,但分配了内存空间。那么这部分内存空间是不是就变成垃圾了呢?
➢不是。因为子类有一部分方法是从父类继承来的,这部分方法是可以用来操作这部分未继承变量的。(实例变量是静态绑定的,这点理解去看上转型对象部分,我悟了!)

子类创建对象时,子类的构造方法总是先调用父类的某个构造方法,完成父类部分的创建;然后再调用子类自己的构造方法,完成子类部分的创建。

父类的所有的成员变量也都分配了内存空间,但子类只能操作继承的那部分成员变量 。

成员变量的隐藏和方法重写

如果子类中声明的成员变量和父类中的成员变量同名时,子类就隐藏了继承的父类成员变量。
同样,子类通过重写可以隐藏已继承的实例方法。(不允许降低方法的访问权限,但可以提高访问权限)

(访问限制修饰符按访问权限从高到低的排列顺序是:public、protected、友好的、private。如果父类是protected或者public级别,子类重写时只能是public级别

方法重写(Override)的规则:
◆方法名相同。
◆参数列表完全相同(类型、数量、顺序)。
◆返回类型相同或是父类方法返回类型的子类型(协变返回类型)。
◆访问权限相同或者更宽松。
◆不能抛出比父类方法更多或更广泛的检查异常。

关键字super

作用:

  1. 在子类中想使用被子类隐藏的成员变量或方法就可以使用关键字super。
  2. 子类不继承父类的构造方法,因此,子类如果想使用父类的构造方法,必须使用关键字super来调用,而且super必须是子类构造方法中的第一条语句。

关键字final

  1. final关键字可以修饰类、成员变量和方法中的局部变量。
  2. final类不能被继承,即不能有子类。
  3. 如果用final修饰父类中的一个方法,那么这个方法不允许子类重写。
  4. 如果成员变量或局部变量被修饰为final的,就是常量。

    ✓ 对于基本数据类型的变量,这意味着其值不可变;
    ✓ 对于对象引用,这意味着引用本身不可变(即不能指向另一个对象),但对象的内容仍然可以修改。

构造函数不能声明为final,因为构造函数本身就不能加访问修饰符

对象的上转型对象

假设Animal类是Tiger的父类

1
2
3
4
5
Animal a = new Tiger();
//或者
Animal a;
Tiger b = new Tiger();
a = b;

对象的上转型对象的实体是子类负责创建的,但上转型对象会失去原对象的一些属性和功能。

上转型对象不能操作子类新增的成员变量;不能调用子类新增的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal{}
class Dog extends Animal{}

public class test{
public static void main(String[] args){
Animal a = new Animal();
Dog d = new Dog();
Animal ad = new Dog();//上转型对象
System.sout.println(a instanceof Animal);
System.sout.println(b instanceof Dog);
System.sout.println(d instanceof Animal);//true!因为Dog是Animal的子类
System.sout.println(ad instanceof Dog);//true!虽然ad是Animal的对象,但它实际上是Dog对象的引用
System.sout.println(ad instanceof Animal);//true
}
}

方法调用是动态绑定的(基于实际对象类型)。
实例变量是静态绑定的(看调用方法的引用类型,而不是实际对象的类型)

一个很好的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Animal{
int m = 10;
public int seeM{
return m;
}
public int getM{
return m;
}
}
class Dog extends Animal{
int m = 20;
public int seeM{
return m;
}
}

public class E{
public static void main(String args[]){
Animal animal = null;
Dog dog = new Dog();
animal = dog; //上转型对象
System.out.println("%d:%d:%d",dog.seeM(), animal.seeM(), animal.getM());
}
}

正确输出:20:20:10

挑错题(ABCD注释标注的哪行代码有错误?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Animal { 
int m =100;
}
class Dog extends Animal{
double m;
}
public class E {
public static void main(String args[]){
Animal animal = null; //A
Dog dog = new Dog( );
animal = dog; //B
dog.m = 3.14; //C
animal.m = 3.14; //D
}
}

正确答案:D
分析:animalAnimal类型的引用,访问m时遵循「静态绑定」:看Animal类型;

抽象类与抽象方法

用关键字abstract修饰的类称为抽象类。方法即抽象方法。父类Animalmint类型,而3.14double类型;会导致精度丢失。

抽象类

抽象类是一种不能被实例化的类,主要用于定义一个基础框架或通用行为,供其他具体(非抽象)子类继承并实现具体的细节

抽象方法

抽象方法是指没有具体实现的方法,只有方法签名(即方法名、参数列表和返回类型)。抽象方法必须在抽象类中声明,并且必须由子类提供具体的实现

为什么要使用抽象类和抽象方法?

  1. 强制规范:通过定义抽象方法,确保所有子类都实现特定的行为。
  2. 代码复用:可以在抽象类中提供一些通用的功能实现,避免重复代码。
  3. 灵活性:子类可以根据自己的需求实现抽象方法,同时可以使用父类提供的其他功能。

面向抽象编程

面向抽象编程,是指编程时依赖“抽象”,而不是依赖“具体实现类”。
◆在设计一个程序时,可以先声明一个abstract类,通过在类中声明若干个abstract方法,表明这些方法在整个系统设计中的重要性,方法体的内容细节由它的非abstract子类去完成。
利用多态实现编程。使用多态进行程序设计的核心技术是使用方法重写上转型对象,即将abstract类声明对象作为其子类的上转型对象,那么这个上转型对象就可以调用子类重写的方法。

abstract与interface区别

特性 接口(Interface) 抽象类(Abstract Class)
修饰符 只能用public(默认也是 public) 可使用public/protected/default(包访问)
成员变量 只能是公共静态常量public static final,(可省略修饰符,编译器自动补充) 可包含任意成员变量(private/protected/public、实例变量 / 静态变量)
成员方法 Java 8 前:仅抽象方法;

Java 8 后:抽象方法、默认方法(default)、静态方法(static);

Java 9 后:新增私有方法(private
可包含抽象方法(abstract)、非抽象方法(普通实例方法、静态方法),也支持私有方法
构造方法 无构造方法(因为接口不能实例化,也无需初始化成员) 有构造方法(用于子类继承时初始化父类成员),但不能直接调用
代码块 不支持静态代码块 / 实例代码块 支持静态代码块和实例代码块

开闭原则

“开-闭原则”(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭

为什么需要开-闭原则?
(1)提高可维护性;
(2)增强灵活性。

内部类

Java支持在一个类中声明另一个类,这样的类称作内部类,而包含内部类的类成为内部类的外嵌类

外嵌类和内部类在编译时,生成两个独立.class文件。

成员内部类

可以访问外部类的所有成员(包括私有成员)。
需要通过外部类的实例来创建成员内部类的实例

1
2
3
4
5
6
7
8
9
10
11
12
public class OuterClass {
private String msg = "Hello, World!";
public class InnerClass { // 成员内部类
public void printMessage() {
System.out.println(msg); // 可以访问外部类的私有成员
}
}
}

OuterClass outer = new OuterClass();//先创建外部类的实例
OuterClass.InnerClass inner = outer.new InnerClass();//成员内部类的声明创建!!!
inner.printMessage(); // 输出: Hello, World!

局部内部类

➢其作用域仅限于该方法或构造方法内部。
➢不能使用访问修饰符(如public, private等),但可以使用final关键字。

匿名内部类

无类名,直接在创建对象时定义。通常用于实现接口或扩展抽象类,特别是在只需要一次性使用的场景下。
➢不能定义构造方法(因为没有类名)。
➢是内部类的特殊形式,仅能用一次
可以继承父类和实现接口。(如接口临时实现、父类方法临时重写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OuterClass {
interface Message {
void greet();
}

public void sayHello() {
Message message = new Message() { // 匿名内部类
public void greet() {
System.out.println("Hello from anonymous inner class!");
}
};
message.greet();
}
}

new Message() { ... }创建了一个匿名类,直接实现了Message接口,并重写了其中的greet()方法。

Java允许直接用接口名和一个类体创建一个匿名对象,此类体被认为是实现了Computable接口的类去掉类声明后的类体,称作匿名类。

static内部类

➢使用static关键字定义的内部类。
不需要外部类的实例即可创建对象(不依赖外嵌类实例)
仅能访问外嵌类静态成员

成员内部类、局部内部类、匿名类不能定义静态成员(仅静态内部类可以)

1
2
3
4
5
6
7
8
9
10
public class OuterClass {
private static String msg = "Hello, Static World!";
public static class StaticNestedClass { // 静态内部类
public void printMessage() {
System.out.println(msg); // 只能访问外部类的静态成员
}
}
}
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass(); //不需要OuterClass实例化,直接创建内部类对象
nested.printMessage(); // 输出: Hello, Static World!

匿名类

匿名类(Anonymous Class)是Java中一种没有显式类名的内部类,核心作用是在创建对象的同时直接定义类的实现,省去了单独定义类的代码冗余。它专为只需要使用一次的临时类场景设计,是Java简化代码的重要语法糖之一。

特点

◆匿名类一定是内部类。
◆可访问外嵌类的成员,不能定义静态成员。
◆必须继承一个父类实现一个接口,且只能创建一个对象。它的构造逻辑完全依赖 “父类 / 接口”,自身不能显式写构造方法(无类名)

匿名类要创建对象,必须有构造方法(否则无法初始化),这个构造方法由编译器隐式生成,我们看不到但实际存在。

匿名类继承父类后,创建匿名类对象时,必须调用父类的构造方法(无参或带参)来初始化父类的成员,这和普通子类创建对象时 “先调用父类构造” 的规则一致。

Java允许直接用接口名和一个类体创建一个匿名对象,此类体被认为是实现了Computable接口的类去掉类声明后的类体,称作匿名类。

分类

(1)与子类相关的匿名类
本质:父类的匿名子类,创建对象时直接重写父类方法。
语法:父类名 对象名 = new 父类名 (){ 重写的方法体};
特点:继承、临时重写、依赖外嵌类成员。

(2)与接口相关的匿名类
本质:实现接口的匿名类,创建对象时直接实现接口抽象方法。
语法:接口名 对象名 = new 接口名 (){ 实现的方法体};
替代方案:Lambda表达式(仅适用于「函数接口」,即只有一个抽象方法的接口)
Lambda语法:(参数列表)->{ 方法体 }(简化匿名类代码)

用Lambda表达式代替匿名类:如果一个接口被标记为函数接口,那么你可以使用Lambda表达式来实现这个接口。

1
2
3
4
5
6
//假设有一个函数式接口
interface SpeakHello {
void speak();
}
//使用Lambda表达式
SpeakHello hello = () -> System.out.println("hello, you are welcome!");

异常类

异常类一般分为两大类:
Error
Exception
异常对象:异常发生时 JVM 自动创建,携带错误信息(消息、堆栈跟踪)。

异常对象常用方法

  1. e.getMessage ():获取异常具体消息(给用户看);
  2. e.printStackTrace ():打印异常堆栈(给开发者调试,显示异常发生路径);
  3. e.toString ():获取异常类型 + 消息(用于日志记录)。

try-catch-finally语句

try 块:包含可能会抛出异常的代码。如果在try块中的任何语句抛出了一个异常,程序会立即跳转到匹配的catch

在多 try-catch 结构中,如果一个 try块中捕获了异常,这不会直接影响其他独立的 try 块中的代码执行。

catch 块:每个catch块都指定了要捕获的异常类型。当try块内抛出的异常与某个catch块指定的异常类型匹配时,该catch块将被执行。可以有多个catch块来处理不同类型的异常。

finally 块:无论 try 块是否抛异常、catch 块是否执行,都会执行(特殊情况除外)

基本语法:

1
2
3
4
5
6
7
8
9
try {
// 可能发生异常的代码
} catch (FileNotFoundException e) { // 特定的异常类型
e.printStackTrace();
} catch (IOException e) { // 更广泛的异常类型
e.printStackTrace();
} finally{
// 无论是否发生异常都会执行的代码
}

但需要注意以下两种特殊情况:
(1)如果在trycatch语句中执行了return语句,那么finally子语句仍然会被执行先执行finally,再return)。
(2)**try
catch语句中执行了程序退出代码,即执行System.exit(0);,则不执行finally子语句(当然包括其后的所有语句)**。

异常抛出throw与throws

关键字 作用 位置 核心用法
throw 手动抛出异常对象 方法体内部 主动抛出异常(如检测到非法参数时)

例:throw new IOException ("故意抛异常");
throws 声明方法可能抛出的异常 方法签名后(方法名 + 参数列表后) 告诉调用者 “该方法可能抛异常,需处理”

例:public void test () throws IOException, MyException {}

自定义异常类

标准异常类无法描述特殊业务错误(如 “收入和支出不能同号”),需自定义异常。

实现步骤

  1. 自定义类继承「Exception」(受检异常)或其子类
  2. 提供构造方法,调用父类构造方法传递异常消息
  3. 在方法中用「throw」抛出自定义异常,用「throws」声明异常

实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 1.自定义异常类
class BankException extends Exception {
public BankException(String message) {
super(message); // 传递异常消息给父类
}
}
// 2.方法中抛出/声明异常
public void income(int in, int out) throws BankException {
if (in <= 0 || out >= 0) { // 业务错误条件
throw new BankException("入账不能为负,支出不能为正"); // 手动抛异常
}
}

断言

断言(Assertions)常用于开发和调试阶段以捕获程序中的逻辑错误。断言语句可以用来验证程序的某些状态是否如预期那样正确(真),如果条件不成立,则抛出一个AssertionError异常,终止程序。

语法

  1. assert 布尔表达式。例如assert score >= 0;
  2. assert 布尔表达式:异常消息。例如assert score >= 0 : "Error!负数不能是成绩";

注意

  1. 默认情况下,JVM禁用断言(断言语句不执行)。
  2. 启用断言:运行程序时加参数「-ea」(enableassertions);例如java -ea 类名.java
  3. 断言仅用于调试,生产环境禁用(避免影响性能)。

示例代码:

1
2
3
4
5
6
7
8
public class TestAssertion {
public static void main(String[] args) {
int x = 10;
assert x > 0 : "x must be greater than 0";//断言语句
System.out.println("Value of x: " + x);
// 禁用断言时,下面的代码正常执行;启用断言且x不大于0时,会抛出AssertionError
}
}

解释:x > 0布尔表达式,是断言的检查条件。若为true,断言通过,程序继续正常执行;若为false,断言失败,抛出 java.lang.AssertionError 异常。"x must be greater than 0"自定义错误消息,可以是任意表达式(字符串、基本类型、对象等),当断言失败时,该消息会被传入AssertionError的构造方法,作为异常的提示信息,方便开发者定位问题。

常用实用类

String类

核心特点

  1. 位于java.lang包,默认导入,被声明为final类,不可继承,用来处理字符序列
  2. 字符串有两种创建方式,常量池创建和堆内存创建,前者可共享,后者每次生成新对象
  3. 字符串不可变。一旦创建,字符序列无法修改,拼接、替换等操作会生成新 String 对象。

字符串两种创建方式的区别

创建方式 存储位置 引用对比(== 适用场景
常量赋值 字符串常量池 相同内容引用相同(返回true 固定不变的字符串
new关键字 堆内存 相同内容引用不同(返回false 动态生成的字符串

常量池工作方式

1
2
3
4
String str1 == "st";
String str2 == "st";

System.out.println(str1 == str2); // true, 因为指向同一个常量池中的对象

step1:JVM先处理str1,由于字符串常量池里面没有找到“st”,所以JVM会在常量池中创建一个新的String对象“st”,并将其引用赋给str1
step2:接下来处理str2,JVM检查字符串常量池,发现已经存在 “st”这个字符串,于是直接将之前创建的 “st” 的引用赋给 str2。

String常量本质也是对象,所以也有自己的引用和实体。

堆内存工作方式

1
2
3
4
String s = new String("xixi");
String t = new String("xixi");

System.out.println(str1 == str2);//False

凡是new运算符构造出的对象都不在常量池,new运算符如它名字一样,每次都要在堆内存中开辟新天地。

也可以用一个已创建的String对象创建另一个String对象。

1
2
3
4
5
6
7
8
9
10
11
12
//创建一个String常量对象
String s = "love";

//创建一个String对象(堆内存工作方式),并传入刚刚的常量对象
String t = new String("s");

System.out.println("s: " + s); //s:love
System.out.println("t: " + t); //t:love

System.out.println("s == t: " + (s == tom)); //false,因为它们是不同的对象

System.out.println("s.equals(t): " + s.equals(tom)); //true,因为内容相同

还有两个较常用的构造方法:

1
2
3
4
5
6
7
char a[] = {'j','a','v','a'};
String s = new String(a);
//等价于String s = new String("java");

char a[] = {'h','e','l','l','o'};
String s = new String(a,2,3);//a数组下标2开始截取3个字符
//等价于String s = new String("llo")

注意:System.out.println(s)输出的是对象的实体。int address = System.identityHashCode(s)返回String对象s的引用。

字符串的并置

字符串并置指通过+运算符或concat()方法,将两个或多个字符串首尾相接生成新字符串的操作。

如果参与并置运算的String对象,只要有一个是变量,那么Java就会在动态区存放所得到的新String对象的实体和引用(new String()

1
2
3
4
String a = "你";
String str3 = a + "好"; // 含变量a,结果存入堆内存
String str4 = "你好";
System.out.println(str3 == str4); // false(堆对象 vs 常量池对象)

纯常量拼接:直接将拼接结果存入字符串常量池,若池中有相同内容则直接复用引用,无需创建新对象

1
2
3
String str1 = "你" + "好"; // 编译器优化为 String str1 = "你好"
String str2 = "你好";
System.out.println(str1 == str2); // true(引用同一常量池对象)

String类常用方法

equals(String s):判断当前 String 对象的字符序列与参数s的字符序列是否完全相同(区分大小写)。
compareTo(String s):按Unicode字典顺序比较两个字符串的字符序列,返回整数结果(正→当前字符串大,负→当前字符串小,0→相等)。
compareToIgnoreCase(String s):比较两个字符串的字典顺序,但忽略大小写。
startsWith(String s)/endsWith(String s):分别判断当前字符串是否以参数s为前缀 / 后缀。
indexOf(String str):从字符串起始位置(索引 0)开始,检索子串str首次出现的位置,未找到返回-1
indexOf(String str, int startPoint):从指定索引startPoint开始,检索子串str首次出现的位置,未找到返回-1
contains(String s):判断当前字符串的字符序列是否包含子串s,返回布尔值。
substring(int startpoint):从指定索引startpoint开始,截取到字符串末尾的子串,返回新 String 对象。
substring(int start, int end):从索引start(包含)截取到索引end(不包含)的子串,返回新 String 对象(左闭右开区间)。
trim():去除字符串首尾的空格(不处理中间空格),返回新 String 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"Hello".equals("hello"); //false

"abc".compareTo("abd"); //-1,说明c的Unicode的值小于d

"天气预报".startsWith("天气"); //true
"test.txt".endsWith("doc"); //false

"I am a good cat".indexOf("a", 7); //13,从索引7开始找第一次出现的a的位置(空字符也算)

tom="student";
tom.contains("stu"); //true

"Hello, World!".substring(0, 5); //Hello

" Java 编程 ".trim(); //Java 编程

字符串向基本数据转化

➢Java.lang包中的Integer类调用其类方法parseInt(Strings)

1
2
3
int x;
String s = "876";
x = Integer.parseInt(s);

类似地,可以使用String类的类方法,valueOf()

正则表达式

核心概念

正则表达式(Regular Expression,简称 Regex)是一套用于描述字符串模式的语法规则。通过 “普通字符 + 元字符” 的组合实现对字符串的匹配、查找、替换、分解四大核心操作。其核心价值是简化复杂字符串处理逻辑(如验证手机号、提取数字、清理特殊字符),避免重复编写大量判断代码

例如:正则表达式"\\dcat"中,\\d是元字符(代表 0-9 任意数字),cat是普通字符,整体可匹配"0cat"1cat”…“9cat

元字符

三种分类:
1.基础元字符(匹配单个字符)

元字符 含义 示例
\\d 匹配 0-9 任意一个数字(digit) \\d可匹配”5”“8”,不匹配”a”
\\D 匹配非数字字符(反义) \\D可匹配”a”“#”,不匹配”3”
\\w 匹配字母、数字、下划线(word) \\w可匹配”A”“5”“_”,不匹配”@”
\\W 匹配非字母、数字、下划线(反义) \\W可匹配”@”“&”,不匹配”b”
.(点号) 匹配任意单个字符(除换行符) "h.l"可匹配”hal”“hbl”,不匹配”hl”
2.方括号元字符(自定义字符集合)
通过[]包裹字符,定义 “匹配集合内任意一个字符”。
表达式 含义 示例
[abc] 匹配 a、b、c 中的任意一个 "[abc]at"可匹配"aat"“bat”
[^abc] 匹配除 a、b、c 外的任意字符(^ 表否定) "[^abc]at"可匹配"dat"“eat”
[a-zA-Z] 匹配任意大小写英文字母 "[a-zA-Z]123"可匹配"A123"“b123”
[0-9] 等价于\\d,匹配 0-9 数字 "[0-9]cat"等价于"\\dcat"

3.量词元字符(匹配字符出现次数)
控制前一个字符或子表达式的匹配次数,如整数、浮点数匹配:

量词 含义 示例
* 匹配前一个字符 0 次或多次 "a*b"可匹配"b"“ab”“aaab”
+ 匹配前一个字符 1 次或多次 "a+b"可匹配"ab"“aaab”,不匹配"b"
? 匹配前一个字符 0 次或 1 次 "a?b"可匹配"b"“ab”,不匹配"aab"
{n} 匹配前一个字符恰好 n 次 "a{2}b"可匹配"aab",不匹配"ab"
{n,} 匹配前一个字符至少 n 次 "a{2,}b"可匹配"aab"“aaab”
{n,m} 匹配前一个字符 n 到 m 次 "a{2,3}b"可匹配"aab"“aaab”
应用场景:
①匹配整数(十进制)的正表达式regex: String regex = "-?[1-9]\\d*";

-?:?表示匹配前面的-(负号)0 次或 1 次

②匹配浮点数的正表达式regex:String regex = "-?[0-9][0-9]*[.][0-9]+";

  1. 第一个[0-9]:表示匹配0、1、2…9 中的任意一个数字,首位可以是 0;
  2. 第二个[0-9]**是量词,代表 “匹配前面的字符 0 次或多次”,这里就是匹配 0 个或多个 0-9 的数字,是对第一个数字的后续补充

③匹配email的正表达式regex: String regex = "\\w+@\\w+\\.[a-z]+(\\.[a-z]+)?";

字符串的替换

String replaceAll(String regex, String replacement)是 Java String类的核心方法之一,用于按照正则表达式匹配的规则,替换字符串中所有符合匹配条件的子串,返回替换后的新字符串。
例如:

1
String str ="12hello567bird".replaceAll("[a-zA-Z]+","你好"); //“12你好567你好”

字符串的分解

split(String regex)是 Java String类的核心方法,用于按照指定的正则表达式作为分隔符,将原字符串拆分为子字符串数组

案例1:

1
2
3
4
String str = "2006年1月6日是我的生日";
String regex = "//D+";
String digitWord[] = str.split(regex);
//digitWord[0]=2006,digitaWord[1]=1

解释:

  1. \\D的含义:正则表达式中,\\D是元字符,代表匹配任意一个非数字字符(等价于[^0-9]);+是量词,代表匹配前面的字符 / 元字符 1 次或多次。因此\\D+的整体含义是:匹配 1 个及以上连续的非数字字符
  2. split()的分割逻辑:split(regex)会将字符串中所有匹配regex的子串作为 “分隔符”,然后把分隔符之间的内容拆分为数组元素;如果字符串开头 / 结尾没有匹配regex的内容,也会正常拆分,且忽略末尾的空字符串

split()方法的核心规则之一:如果字符串的前缀直接匹配分隔符正则 ,那么分隔符左侧没有任何内容,会生成一个空字符串""作为数组的第一个元素;而如果前缀是目标内容,则直接从目标内容开始拆分,不会产生空串。

案例2:

1
2
3
String str1 = "公元2006年1月6日是我的生日";
String regex = "//D+";
String word[] = str1.split(regex);//word[0]="",word[1]="2006",word[2]="1",word[3]="6"
案例 原字符串 字符串前缀内容 前缀是否匹配\\D+(非数字字符 1 次及以上)
案例 1 "2006年1月6日是我的生日" 2006(数字) ❌ 不匹配
案例 2 "公元2006年1月6日是我的生日" 公元(非数字) ✅ 匹配

StringTokenizer类

StringTokenizer 类是 Java java.util包中专门用于字符串分解的工具类,核心作用是按指定分隔标记拆分字符串,获取其中的 “单词”(语言符号)。它不依赖正则表达式

构造函数

StringTokenizer 类提供两个核心构造方法,用于初始化字符串分析器:

构造方法 语法格式 含义解释 示例
无参分隔符 StringTokenizer(String s) 为字符串s创建分析器,使用默认分隔标记(空白字符,换行符,回车符,Tab符) new StringTokenizer("we are students") → 按空格拆分
自定义分隔符 StringTokenizer(String s, String delim) 为字符串s创建分析器,delim中的任意字符(或组合) 作为分隔标记 new StringTokenizer("you#*are*##welcome", "#*") → 按#*的任意组合拆分

注意:delim参数是 “字符集合”,而非 “字符串”:例如delim="#*"表示分隔标记是#*,或二者的任意连续组合(如#****##等)。

核心方法

方法名 语法格式 功能描述 返回值
hasMoreTokens() boolean hasMoreTokens() 判断字符串中是否还有未提取的单词 true(有下一个单词)/false(无)
nextToken() String nextToken() 提取并返回下一个单词,同时移动指针 下一个单词的字符串
countTokens() int countTokens() 统计当前剩余未提取的单词总数 剩余单词的个数(int 类型)
1
2
3
4
5
6
7
8
StringTokenizer tokenizer = new StringTokenizer(We are family!); //创建对象

while(tokenizer.hasMoreTokens()){
String word = tokenizer.nextToken(); //获取下一个单词
System.out.println("word");
}

int total = tokenizer.countTokrns(); //统计单词总数

Scanner类

Scanner 类是 Java java.util包中用于解析输入数据的核心工具类,既能读取键盘输入(交互式输入),也能解析字符串、文件等数据源中的数据,支持按自定义分隔符提取不同类型的信息(如整数、浮点数、字符串),是日常开发中处理输入解析的高频工具。

构造方法

构造方法 语法格式 含义解释 示例
解析字符串 Scanner(String source) 绑定字符串数据源,解析该字符串中的数据 new Scanner("I Love Java 123")
解析键盘输入(交互式) Scanner(InputStream source) 绑定字节输入流数据源,最常用System.in(代表键盘输入),实现交互式输入 new Scanner(System.in)
代码实例:
1
2
3
4
String str = "xixi";
Scanner strScanner1 = new Scanner(str);

Scanner strScanner2 = new Scanner(System.in); //手动输入

核心方法

1.分隔符相关

方法名 语法格式 功能描述
useDelimiter() Scanner useDelimiter(String regex) 用正则表达式regex作为分隔标记,替代默认的空白字符分隔符
delimiter() Pattern delimiter() 返回当前使用的分隔符(以Pattern对象形式,需调用pattern()获取正则字符串)
1
2
3
Scanner scanner = new Scanner("市话76.8元,长途167.38元");
// 用正则"[^0-9.]+"(匹配非数字、非小数点的字符)作为分隔符,提取价格
scanner.useDelimiter("[^0-9.]+");

2.数据读取方法

方法类别 方法名 功能描述 示例
字符串读取 next() 读取下一个单词(从当前位置到下一个分隔符之间的内容,不包含分隔符) 解析"Hello World"时,next()返回"Hello"
nextLine() 读取一整行内容(从当前位置到换行符\n,包含换行符前的所有字符) 读取键盘输入的一行文本
基本类型读取 nextInt() 读取下一个单词,并转换为int类型 解析"123"时,返回123int类型)
nextDouble() 读取下一个单词,并转换为double类型 解析"76.8"时,返回76.8double类型)
nextBoolean() 读取"true""false",转换为boolean类型 解析"true"时,返回true
3.判断数据是否存在的方法
在读取数据前,先用以下方法判断目标类型的数据是否存在,可避免NoSuchElementException(无数据可读取)或InputMismatchException(类型不匹配):
方法名 语法格式 功能描述
hasNext() boolean hasNext() 判断当前数据源中是否还有未读取的单词(无论类型,只要有内容)
hasNextInt() boolean hasNextInt() 判断下一个单词是否能转换为int类型
hasNextDouble() boolean hasNextDouble() 判断下一个单词是否能转换为double类型
hasNextLine() boolean hasNextLine() 判断当前数据源中是否还有未读取的整行内容

代码实例:

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
import java.util.Scanner;

public class ScannerPriceDemo {
public static void main(String[] args) {
// 数据源:包含价格的字符串
String priceStr = "市话76.8元,长途167.38元,短信12.68元";
Scanner scanner = new Scanner(priceStr);

// 自定义分隔符:匹配非数字、非小数点的字符(正则"[^0-9.]+")
scanner.useDelimiter("[^0-9.]+");

double total = 0.0;
System.out.println("提取的价格:");

// 循环读取所有价格(判断是否有下一个double类型数据)
while (scanner.hasNextDouble()) {
double price = scanner.nextDouble();
System.out.println(price);//76.8, 167.38, 12.68
total += price;
}

System.out.println("总价:" + total); // 输出:256.86
scanner.close(); // 使用完毕后关闭,释放资源
}
}

日期与时间

Java 中日期与时间的处理主要依赖 Java 8+ 引入的java.time(如LocalDateLocalTimeLocalDateTime),该包解决了传统Date/Calendar类的线程不安全、API 设计混乱等问题,是当前开发的标准工具。

核心类

类名 核心功能 包含字段 典型用法
LocalDate 处理仅日期(无时间、无时区) 年、月、日 生日、订单日期、节假日判断
LocalTime 处理仅时间(无日期、无时区) 小时、分钟、秒、纳秒 闹钟时间、会议开始时间(不含日期)
LocalDateTime 处理日期 + 时间(无时区) 年、月、日、小时、分钟、秒、纳秒 订单创建时间、日志时间戳

当前日期和时间通过now方法获取

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.time.LocalDate;  
import java.time.LocalTime;
import java.time.LocalDateTime;

class DataTimeInit {
public static void main(String[] args) {
LocalDate currentDate = LocalDate.now();
LocalTime currentTime = LocalTime.now();
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("当前日期:" + currentDate);
System.out.println("当前时间:" + currentTime);
System.out.println("当前日期时间:" + currentDateTime);
}
}

运行结果:

1
2
3
当前日期:2025-11-30
当前时间:00:15:43.989294400
当前日期时间:2025-11-30T00:15:43.989294400

创建指定的时间日期通过of()方法指定

代码示例:

1
2
LocalData Data = LocalData.of(2021, 10, 31);
LocalTime Time = LocalTime.of(22, 30, 32);

核心操作

1.日期的修改
由于LocalDate等类是不可变的,修改操作(如增减年 / 月 / 日)不会改变原对象,而是返回一个新对象,需要用变量接收结果。

操作类型 方法示例(以LocalDate为例) 说明
增减年份 date.plusYears(18) / date.minusYears(1) 增加 18 年 / 减少 1 年
增减月份 date.plusMonths(23) / date.minusMonths(1) 增加 23 个月 / 减少 1 个月
增减天数 date.plusDays(8976) / date.minusDays(10) 增加 8976 天 / 减少 10 天
调整到指定字段 date.withYear(2007) / date.withMonth(5) 将年份调整为 2007 / 将月份调整为 5 月
2.日期的比较
通过isAfter()/isBefore()/isEqual()方法判断两个日期 / 时间的先后关系,返回boolean值。
1
2
3
4
5
6
7
8
9
10
11
12
13
LocalDate dateA = LocalDate.of(2025, 3, 18);
LocalDate dateB = LocalDate.of(2025, 1, 22);

// 判断dateA是否在dateB之后
boolean isAfter = dateA.isAfter(dateB);
// 判断dateA是否在dateB之前
boolean isBefore = dateA.isBefore(dateB);
// 判断dateA与dateB是否相等
boolean isEqual = dateA.isEqual(dateB);

System.out.println("2025-03-18在2025-01-22之后:" + isAfter); // true
System.out.println("2025-03-18在2025-01-22之前:" + isBefore); // false
System.out.println("2025-03-18和2025-01-22相同:" + isEqual); // false

3.日期的格式化
String.format()格式化占位符

占位符 含义 示例(日期 2025-03-18,时间 23:30:01)
%tY 4 位年份 2025
%tm 2 位月份(01-12) 03
%td 2 位日期(01-31) 18
%tH 24 小时制小时(00-23) 23
%tM 2 位分钟(00-59) 30
%tS 2 位秒(00-60) 01
1
2
3
4
5
6
7
8
9
10
11
//获取当前日期
LocalDate date = LocalDate.now();
System.out.println(date);//2025-11-30

String formattedDate = String.format("今天的日期是:%1$tY年%1$tm月%1$td日", date);
System.out.println(formattedDate); //今天的日期是:2025年11月30日


LocalDateTime dateTime = LocalDateTime.of(2025, 3, 18, 23, 30, 01);
String formattedDateTime = String.format("当前时间:%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS", dateTime);
System.out.println(formattedDateTime); // 当前时间:2025-03-18 23:30:01

4.时间差计算
LocalDate/LocalTime/LocalDateTime均提供until()方法,用于计算两个时间之间的差值,需指定结束时间时间单位(如天、月、年),返回long类型的差值。
语法:long diff = 起始时间.until(结束时间, 时间单位);
时间单位又是什么呢?(ChronoUnit枚举)

  • ChronoUnit.YEARS:年
  • ChronoUnit.MONTHS:月
  • ChronoUnit.DAYS:天
  • ChronoUnit.HOURS:小时
  • ChronoUnit.MINUTES:分钟
  • ChronoUnit.SECONDS:秒

    import java.time.temporal.ChronoUnit;

例如:计算2021年10月31日到此刻(2025年11月30日)的时间差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.time.LocalDate;  
import java.time.temporal.ChronoUnit;

class DataTimeInit {
public static void main(String[] args) {
LocalDate startDate = LocalDate.of(2021, 10, 31);
LocalDate endDate = LocalDate.now();

// 计算年差
long years = startDate.until(endDate, ChronoUnit.YEARS);
// 计算月差
long months = startDate.until(endDate, ChronoUnit.MONTHS);
// 计算天差
long days = startDate.until(endDate, ChronoUnit.DAYS);
// 计算小时差(需转为LocalDateTime,默认00:00:00)
long hours = startDate.atStartOfDay().until(endDate.atStartOfDay(), ChronoUnit.HOURS);

System.out.println("2021-10-31到此刻(2025年11月30日)相差:");
System.out.println(years + "年");
System.out.println(months + "个月");
System.out.println(days + "天");
System.out.println(hours + "小时");
}
}

运行结果:

1
2
3
4
5
2021-10-31到此刻(2025年11月30日)相差:
4年
48个月
1491天
35784小时

Math类

Math 类位于java.lang包(默认导入),所有方法均为静态方法,无需创建对象即可调用,底层多为native实现(调用 C/C++ 代码,效率极高),是日常开发中最常用的数学工具类。
不可实例化:构造方法被private修饰,无法创建 Math 对象,直接通过Math.方法名()调用;
常用常量

  1. Math.PI:圆周率(约3.14)
  2. Math.E:自然常数(约2.7),用于对数、指数运算。
    方法:
    (1)绝对值与极值运算
方法 语法 功能 示例
abs() static double abs(double a) 返回参数的绝对值(支持int/long/float/double Math.abs(-5.2) → 5.2
max() static double max(double a, double b) 返回两个数的最大值 Math.max(3.8, 5.1) → 5.1
min() static double min(double a, double b) 返回两个数的最小值 Math.min(2, 7) → 2

(2)幂运算与开方

方法 语法 功能 示例
pow() static double pow(double a, double b) 计算ab次幂(a^b Math.pow(2, 3) → 8.0(2³)
sqrt() static double sqrt(double a) 计算a的平方根(a≥0,否则返回NaN Math.sqrt(16) → 4.0
cbrt() static double cbrt(double a) 计算a的立方根(支持负数) Math.cbrt(-8) → -2.0

(3)取整运算

方法 规则 示例(输入→输出)
ceil(double a) 向上取整(取≥a 的最小整数) 2.1→3.0-1.5→-1.0
floor(double a) 向下取整(取≤a 的最大整数) 2.9→2.0-1.2→-2.0
round(double a) 四舍五入(double返回longfloat返回int 2.5→3-3.5→-3
(4)随机数
static double random()返回[0.0, 1.0)之间的伪随机浮点数(包含 0.0,不包含 1.0)
1
2
//可以扩展生成指定范围[min, max)的整数
int randomNum = (int)(Math.radom() * 100) + 1; //生成1~100的随机数

BigInteger类

long类型最大只能表示9223372036854775807(约 9e18),当需要处理更大整数(如密码学、金融计算)时,需使用java.math.BigInteger类,它支持任意精度整数运算,仅受内存限制。

不可变性:对象创建后值无法修改,所有运算(加 / 减 / 乘 / 除)均返回新的BigInteger对象。
方法:

方法 语法 功能 示例
add() BigInteger add(BigInteger val) 加法运算 a.add(b) → a+b
subtract() BigInteger subtract(BigInteger val) 减法运算 a.subtract(b) → a-b
multiply() BigInteger multiply(BigInteger val) 乘法运算 a.multiply(b) → a×b
divide() BigInteger divide(BigInteger val) 除法运算(整除,无小数) BigInteger.valueOf(10).divide(BigInteger.valueOf(3)) → 3
remainder() BigInteger remainder(BigInteger val) 取余运算 BigInteger.valueOf(10).remainder(BigInteger.valueOf(3)) → 1
compareTo() int compareTo(BigInteger val) 比较大小(1→当前大,-1→当前小,0→相等) a.compareTo(b) > 0 → a > b
pow() BigInteger pow(int exponent) 幂运算(当前数的exponent次幂) BigInteger.valueOf(2).pow(10) → 1024

Random类

java.util.Random类是 Java 生成伪随机数的核心类,Math.random()更灵活(支持多种数据类型、指定种子),适用于游戏开发、测试数据生成、随机事件模拟等场景。
伪随机数:生成的随机数序列是 “确定性” 的 —— 只要种子(seed)相同,生成的随机数序列完全一致;

import java.util.Random;

构造方法

构造方法 语法 功能 适用场景
默认种子 Random() 用当前系统时间毫秒数作为种子 需每次运行生成不同随机序列(如游戏随机事件)
指定种子 Random(long seed) 使用seed作为种子创建一个Random对象(如果用相同的种子值多次创建实例,那么将生成完全相同的随机数序列) 重现相同随机结果(如测试固定随机数据)

生成方法

方法 语法 功能 示例
nextInt() int nextInt() 返回任意int值(-2³¹ ~ 2³¹-1) random.nextInt() → -123456(随机正负整数)
nextInt(int bound) int nextInt(int bound) 返回[0, bound)的整数(bound>0 random.nextInt(10) → 0~9的随机整数
nextDouble() double nextDouble() 返回[0.0, 1.0)的浮点数 random.nextDouble() → 0.678
nextBoolean() boolean nextBoolean() 返回随机truefalse(概率各 50%) random.nextBoolean() → true
nextBytes(byte[] bytes) void nextBytes(byte[] bytes) 生成随机字节填充数组 byte[] arr = new byte[5]; random.nextBytes(arr);

代码示例:

1
2
3
4
5
6
7
Random random1 = new Random();  
System.out.println(random1.nextInt(50));//随机1~50的整数

Random random2 = new Random(12);
Random random3 = new Random(12);
System.out.println(random2.nextInt(100)); //随机1~100的整数
System.out.println(random3.nextInt(100)); //结果与random2相同,并且,结果不会再改变

第一次运行结果:

1
2
3
30 
66
66

第二次运行结果:

1
2
3
49
66
66

拓展:生成[min, max)范围的整数(如 1~100)

1
2
int min = 1, max = 100;
int randomNum = ranodm.nextInt(max - min) + min;//公式:nextInt(范围长度) + 最小值

输入流与输出流

【一个讲得很好的视频】33_【IO】IO流的引入_哔哩哔哩_bilibili
input output -> I/O流

File类

File 类是 Java java.io包中用于操作文件和目录属性的核心工具类,其核心作用是 “描述文件 / 目录的信息”,而非直接读写文件内容。它能获取文件路径、大小、权限等属性,也能实现文件 / 目录的创建、删除、遍历等操作,是所有 I/O 流操作的 “前置基础”。

import java.io.*;

构造方法

构造方法 语法格式 含义解释 示例
直接指定路径 File(String pathname) 用字符串路径创建 File 对象,路径可绝对可相对 new File("D:\\letter.txt")(绝对路径)、new File("src\\test.java")(相对路径)
父路径 + 子名称 File(String parent, String child) 父路径为字符串,子名称为文件 / 目录名,自动拼接路径 new File("D:\\myDir", "note.txt") → 路径为D:\myDir\note.txt
父 File 对象 + 子名称 File(File parent, String child) 父目录为已存在的 File 对象,子名称为文件 / 目录名,灵活适配目录复用场景 File parent = new File("D:\\myDir"); new File(parent, "note.txt")

常用方法

方法名 语法 功能描述 示例(文件存在时)
getName() String getName() 获取文件 / 目录的名称(不含路径) new File("D:\\note.txt").getName() → "note.txt"
getAbsolutePath() String getAbsolutePath() 获取绝对路径(从系统根目录到目标的完整路径) 相对路径创建的对象调用后返回完整路径
getParent() String getParent() 获取父目录路径(若没有则返回 null) new File("D:\\myDir\\note.txt").getParent() → "D:\\myDir"
length() long length() 获取文件长度(单位:字节),目录返回 0 文本文件"abc"返回 3L
exists() boolean exists() 判断文件 / 目录是否存在 存在返回true,否则false
isFile() boolean isFile() 判断是否为普通文件(非目录) 文件返回true,目录返回false
isDirectory() boolean isDirectory() 判断是否为目录 目录返回true,文件返回false
canRead() boolean canRead() 判断是否可读 允许读取返回true
canWrite() boolean canWrite() 判断是否可写 允许修改返回true
isHidden() boolean isHidden() 判断是否为隐藏文件 / 目录 隐藏返回true
lastModified() long lastModified() 获取最后修改时间(毫秒时间戳) 需转换为日期格式(如new Date(file.lastModified())

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
File f = new File("src\\Chapter9\\Example1\\tom\\cat","Example9_1.java"); //Windows系统用\(需转义为\\)  
System.out.println(f.getName()+"是可读的吗:\n"+f.canRead());
System.out.println(f.getName()+"的长度:\n"+f.length());
System.out.println(f.getName()+"的绝对路径:\n"+f.getAbsolutePath());

File file = new File("new.txt");
if(file.exists()) {
try {
file.createNewFile();
System.out.println
("在当前目录(运行程序的目录)下创建了新文件:\n"+file.getName());
}
catch(IOException exp){}
}
System.out.println(file.getName()+"的绝对路径:"+file.getAbsolutePath() );

运行结果:

1
2
3
4
5
6
7
8
9
Example9_1.java是可读的吗:
false
Example9_1.java的长度:
747
Example9_1.java的绝对路径:
D:\AllCode\Java\Term3\Start\Class\src\Chapter9\Example1\tom\cat\Example9_1.java
在当前目录(运行程序的目录)下创建了新文件:
new.txt
new.txt的绝对路径:D:\AllCode\Java\Term3\Start\Class\new.txt

目录相关操作

boolean mkdir() → 创建单级目录;
boolean mkdirs() → 创建多级目录(父目录不存在时自动创建)。

1
2
3
4
File dir1 = new File("D:\\myDir");
dir1.mkdir(); // 仅创建myDir,父目录D:必须存在
File dir2 = new File("D:\\a\\b\\c");
dir2.mkdirs(); // 自动创建a、b、c三级目录,父目录不存在也可

boolean createNewFile() → 创建普通文件(目录不存在时抛出异常)。

1
2
3
4
5
6
7
File file = new File("D:\\note.txt");
try {
boolean created = file.createNewFile(); // 父目录D:必须存在
System.out.println(created ? "文件创建成功" : "文件已存在");
} catch (IOException e) {
e.printStackTrace(); // 父目录不存在、权限不足时抛出异常
}

boolean delete() → 删除文件或空目录(非空目录无法直接删除)。

1
2
3
4
File javaDir = new File("D:\\javaTest");  
System.out.println(javaDir.isDirectory()); //是否是已存在的目录
boolean delete = javaDir.delete();
System.out.println(delete? "已删除": "删除失败");

运行可执行文件

通过Runtime类配合File对象,可打开本地可执行文件。
代码示例:

1
2
3
4
5
6
7
8
Runtime runtime;
try {
Runtime runtime = Runtime.getRuntime(); //获取当前运行环境
File pythonIDE = new File("C:\\Users\\惠普\\Downloads\\Edge\\pycharm-community-2025.1.3.1.exe"); //要打开的可执行文件的位置
runtime.exec(pythonIDE.getAbsolutePath()); //
} catch (Exception e) {
e.printStackTrace();
}

核心讲解:exec(String command) 是 Runtime 类的核心方法,功能是将参数 command 作为 “操作系统命令” 执行。此处command是记pythonIDE的绝对路径 C:\Users\惠普\Downloads\Edge\pycharm-community-2025.1.3.1.exe,因此该方法会告诉操作系统:“启动路径为C:\Users\惠普\Downloads\Edge\pycharm-community-2025.1.3.1.exe的程序”。

文件字节输入、输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.创建流对象(关联目标文件)
InputStream in = new FileInputStream("源文件路径"); // 输入流:读文件
OutputStream out = new FileOutputStream("目标文件路径"); // 输出流:写文件

// 2.读写数据(字节数组缓冲提高效率)
byte[] buffer = new byte[1024]; // 1KB缓冲数组
int len; // 实际读取的字节数
while ((len = in.read(buffer)) != -1) { // 读取到缓冲数组,返回-1表示读取结束
out.write(buffer, 0, len); // 将缓冲数组中0~len的字节写入输出流
}

// 3.关闭流(先关输出流,再关输入流)
out.close();
in.close();

文件字符输入、输出流

与字节流(FileInputStream/FileOutputStream)的区别:

对比维度 字符流(FileReader/FileWriter 字节流(FileInputStream/FileOutputStream
操作单位 字符(按编码映射字节) 字节(1 字节 = 8 位)
适用文件 文本文件(.txt.java等) 所有文件(文本、图片、音频等二进制文件)
编码处理 自动处理编码转换,不易乱码 需手动处理编码,读写文本易乱码
核心方法 支持字符串、字符数组读写 仅支持字节、字节数组读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1.创建字符输入流(读源文件)和输出流(写目标文件)
FileReader reader = new FileReader("源文件路径"); // 如"src\\a.txt"
// 输出流追加模式:append=true
FileWriter writer = new FileWriter("目标文件路径", true); // 如"src\\b.txt"

// 2.创建字符缓冲数组(提高效率)
char[] buffer = new char[1024]; // 1KB缓冲,存储字符
int len; // 实际读取的字符数

// 3.循环读取源文件,写入目标文件
while ((len = reader.read(buffer)) != -1) {
// 写入缓冲数组中实际读取的len个字符(避免写入空字符)
writer.write(buffer, 0, len);
writer.flush(); //!!!强制刷新缓冲,确保数据写入!!!
}

// 4.关闭流(先关输出流,再关输入流)
writer.close();
reader.close();

文件锁

文件锁是 Java 中用于控制多进程 / 多线程对同一文件访问权限的机制,核心作用是避免多个程序同时修改文件导致的数据错乱。文件锁的本质是给文件加 “访问权限控制”,让多个程序按规则有序访问文件,避免数据错乱。

两种锁类型

  1. 独占锁(Exclusive Lock,写锁)
    同一时间,仅允许一个程序持有独占锁,其他程序既不能获取独占锁,也不能获取共享锁
  2. 共享锁(Shared Lock,读锁)
    同一时间,允许多个程序持有共享锁,但所有持有共享锁的程序仅能读取文件,不能修改;若已有程序持有共享锁,其他程序不能获取独占锁;

文件锁使用步骤

  1. 创建支持读写的文件流
1
2
// 创建RandomAccessFile流,模式为"rw"(读写权限),否则无法获取锁
RandomAccessFile raf = new RandomAccessFile("D:\\data.txt", "rw");
  1. 从流中获取FileChannel对象
1
2
// 从RandomAccessFile流中获取FileChannel对象
FileChannel channel = raf.getChannel();
  1. 获取文件锁
1
2
3
4
5
6
7
8
9
// 方式1:阻塞获取独占锁(默认对整个文件加锁)
FileLock lock = channel.lock();

// 方式2:非阻塞获取独占锁,需判断是否成功
FileLock lock = channel.tryLock();
if (lock == null) {
System.out.println("文件已被锁定,无法获取锁");
return;
}
  1. 执行文件操作并释放锁
1
2
3
4
5
6
7
8
9
10
11
12
try {
// 执行文件操作(如写入数据)
raf.write("Hello File Lock".getBytes());
} finally {
// 手动释放锁(必须执行)
if (lock != null) {
lock.release();//!!!
}
// 关闭通道和流
channel.close();
raf.close();
}

操作完成后必须手动释放锁FileLock.release()),否则其他程序会一直阻塞,导致资源泄漏;

JDBC

概述

JDBC(Java Database Connectivity)是 Java 提供的一套访问数据库的标准 API—— 简单说,它就是 Java 程序和数据库之间的 “桥梁”。

JDBC 采用 “Java 程序 → JDBC API → 数据库驱动 → 数据库” 的分层结构。

JDBC的API提供了以下接口和类:
DriverManager:这个类管理一系列数据库驱动程序。匹配连接使用通信子协议从 JAVA 应用程序中请求合适的数据库驱动程序。
Connection:此接口具有接触数据库的所有方法。该连接对象表示通信上下文,即,所有与数据库的通信仅通过这个炸接对象进体。

增删改查

代码示例:用 Java 连接 MySQL,查一张表的所有数据并打印。

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
37
38
39
40
41
import java.sql.*; //导入JDBC核心包  

public class Example14_1 {
public static void main(String args[]) {
//说明JDC核心对象
Connection con=null;// 数据库连接对象(相当于Java和MySQL的“通话线路”)
Statement sql; // SQL执行对象
ResultSet rs; //结果集对象

try{ //加载JDBC-MySQL8.0连接器
Class.forName("com.mysql.cj.jdbc.Driver");
}
catch(Exception e){}
//数据库连接地址
String uri = "jdbc:mysql://localhost:3306/Book?"+ "useSSL=false&serverTimezone=CST&characterEncoding=utf-8";
String user ="root";
String password ="";

try{
con = DriverManager.getConnection(uri,user,password); //建立连接
}
catch(SQLException e){
System.out.println(e);
}
try{
sql=con.createStatement(); //用连接对象创建SQL执行对象
rs=sql.executeQuery("SELECT * FROM bookList");//查表,并将数据存入rs
while(rs.next()) {
String number=rs.getString(1);
String name=rs.getString(2);
float price=rs.getFloat(3);
Date date=rs.getDate(4);
System.out.printf("%s,%s,%.2f,%s\n",number,name,price,date);
}
con.close(); //关闭连接
}
catch(SQLException e) {
System.out.println(e);
}
}
}

代码示例:给 bookList 表加 2 本新书;把其中一本叫 “大学英语”的书,出版日期改成 2019-12-26;然后查所有书的信息,打印出来确认操作是否成功。

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
import java.sql.*; //导入JDBC核心包    
public class Example14_4 {
public static void main(String args[]) {
Connection con;
Statement sql;
ResultSet rs;

//调用自定义工具GETDBConnection的connectDB方法,建立连接
con = GetDBConnection.connectDB("Book","root","");
if(con == null ) return; //连接失败则退出程序

String updateRecord =
"update bookList set chubanDate = '2019-12-26' where name='大学英语'";
String record = "('2-306-08465-7','春天', 35.8,'2020-3-20'),"+"('5-777-56462-9','冬日', 29.9,'2019-12-23')";
String addRecord ="insert into bookList values"+record;
try {
sql=con.createStatement(); //用连接对象创建SQL执行对象
int ok = sql.executeUpdate(addRecord); //2。执行新增,executeUpdate()用于增删改,返回“影响的行数”
ok =sql.executeUpdate(updateRecord); //1。执行修改
rs = sql.executeQuery("select * from bookList");
while(rs.next()) {
String ISBN = rs.getString(1);
String name = rs.getString(2);
String price = rs.getString(3);
String date = rs.getString(4);
System.out.printf("%s,%s,%s,%s.\n",ISBN,name,price,date);
}
con.close(); //关闭连接
}
catch(SQLException e) {
System.out.println("记录中的ISBN值不能重复"+e);
}
}
}

上一个案例是手动写 DriverManager.getConnection(...) 建立连接,这个案例用了 自定义工具类 GetDBConnection:把 “加载驱动、建立连接” 的重复代码封装起来,后续用的时候直接调用 connectDB(数据库名, 用户名, 密码) 就行,简化开发。
执行增删改 SQL:用 executeUpdate() 方法,返回 “影响的行数”(比如新增 2 条就返回 2,修改 1 条就返回 1)
执行查询 SQL:用 executeQuery() 查全表数据,结果存入 rs
遍历结果集:用rs.next() 移动指针,逐行获取数据并打印,确认新增和修改是否生效;

预处理

 Statement:每次执行 SQL 都要 “拼接 SQL 字符串 → 数据库编译 SQL生成内部命令 → 执行”。
 PreparedStatement:对参数sql指定的SQL语句进行预编译处理,生成数据库底层的内部命令,并封装在该对象中,只要调用就能直接执行。