Java教程

基础

Java技术体系

  • JavaSE(Java Platform, Standard Edition标准版):允许您在桌面和服务器上开发和部署Java应用程序。Java提供了丰富的用户界面、性能、多功能性、可移植性和当今应用程序所需的安全性。
  • JavaEE(Java Platform, Enterprise Edition企业版):是为开发企业环境下的应用程序提供的一套解决方案,主要针对于Web应用程序开发。
  • JavaME(Java Platform, Micro Edition 小型版):为互联网上的嵌入式和移动设备上运行的应用提供了一个健壮、灵活的环境:微控制器、传感器、网关、移动电话、个人数字助理(PDA)、电视机顶盒、打印机等等。JavaME包括灵活的用户界面、健壮的安全性、内置的网络协议,以及支持动态下载的网络和离线应用程序。基于JavaME的应用程序在许多设备上都是可移植的,但是利用了每个设备的本机功能。
  • Java Embedded: 用于解锁物联网的智能设备的价值: 通过远程市场更新和刷新功能延长产品生命周期和价值; 利用Java的可伸缩性、健壮性、可移植性和全套功能,提高生产效率,降低成本,缩短上市时间; 在边缘启用快速数据功能;
  • Java Card:使安全元件(如智能卡和其他防篡改安全芯片)能够承载采用Java技术的应用程序。Java card提供了一个安全的、可互操作的执行平台,它可以在一个资源受限的设备上存储和更新多个应用程序,同时保持最高的认证级别和与标准的兼容性。
  • Java TV:是一种基于JavaME的技术,它为开发在tv和机顶盒设备上运行的java应用程序提供了一个性能良好、安全且易于实现的解决方案。使用Java TV运行时,开发人员可以轻松创建应用程序,例如电子节目指南(EPG)、视频点播(VOD)客户端、游戏和教育应用程序、用于访问Internet数据的应用程序(例如天气、新闻播报器、社交网络)以及大多数蓝光光盘标题上的用户界面和奖金内容。

JVM、JRE、JDK的关系

  • JVM(Java Virtual Machine ):Java虚拟机,是运行所有Java程序的假想计算机,是Java程序的运行环境之一,也是Java 最具吸引力的特性之一。我们编写的Java代码,都运行在JVM 之上。
  • JRE (Java Runtime Environment) :是Java程序的运行时环境,包含JVM 和运行时所需要的核心类库
  • JDK (Java Development’s Kit):是Java程序开发工具包,包含JRE 和开发人员使用的工具。

配置JDK环境

  1. 下载jdk
    JDK下载地址链接:JDK1.7
  2. 配置环境变量
    #vi $HOME/.bash_profile
    export JAVA_HOME=/opt/jdk1.7.0
    export PATH=$JAVA_HOME/bin:$PATH
  3. 测试java
    source $HOME/.bash_profile
    java -version

注释

注释单行

// 注释内容

注释多行

/*
注释内容
*/

文档注释

/**
注释内容
*/

常见的注释:

注释方法 描述 示例
@author 标明开发该类模块的作者,多个作者之间使用,分割 @author John, Jane
@version 标明该类模块的版本 @version 1.0
@see 参考转向,也就是相关主题 @see SomeClass
@since 从哪个版本开始增加的 @since 2.3.0
@param 对方法中某参数的说明 格式:@param 形参名 形参类型 形参说明
@return 对方法返回值的说明 格式:@return 返回值的说明
@throws 对方法可能抛出的异常进行说明 格式:@throws SomeException 异常说明

关键字

关键字 描述
abstract 用于声明抽象类或抽象方法。
assert 用于调试目的,配合断言语句使用。
boolean 基本数据类型,表示真或假。
break 用于跳出循环或switch语句。
byte 基本数据类型,表示8位整数。
case 在switch语句中用于指定特定的情况。
catch 用于捕获和处理try-catch块中的异常。
char 基本数据类型,表示单个字符。
class 用于声明类。
const 已弃用,曾用于声明常量。
continue 结束当前循环的迭代并继续下一次迭代。
default 在switch语句中的默认情况。
do 用于创建do-while循环。
double 基本数据类型,表示双精度浮点数。
else 在if语句中的其他分支。
enum 用于声明枚举类型。
extends 用于指定类继承的父类。
final 用于声明不可更改的类、方法或变量。
finally 用于定义无论异常是否被捕获都会执行的代码块。
float 基本数据类型,表示单精度浮点数。
for 用于创建for循环。
goto 已弃用,曾用于无条件跳转。
if 用于创建条件语句。
implements 用于类实现接口。
import 用于导入其他包中的类。
instanceof 用于测试对象是否为特定类的实例。
int 基本数据类型,表示整数。
interface 用于声明接口。
long 基本数据类型,表示长整数。
native 用于声明使用本地代码实现的方法。
new 用于创建对象。
null 表示空引用。
package 用于声明包。
private 用于声明私有字段或方法。
protected 用于声明受保护字段或方法。
public 用于声明公共字段或方法。
return 用于从方法中返回值。
short 基本数据类型,表示短整数。
static 用于声明静态字段、方法或块。
strictfp 用于强制执行浮点运算的严格规范。
super 用于访问父类的成员或调用父类的构造方法。
switch 用于创建多路分支语句。
synchronized 用于同步访问代码块或方法。
this 用于访问当前对象的成员或调用当前类的构造方法。
throw 用于抛出异常。
throws 用于声明方法可能抛出的异常。
transient 用于声明不需要序列化的字段。
try 用于创建异常处理块。
void 用于声明不返回值的方法。
volatile 用于声明易变字段,保证多线程同步访问。
while 用于创建while循环。

标识符

给类、变量、方法、包等命名的字符序列,称为标识符。

标识符命名规则

  • Java的标识符只能使用26个英文字母大小写,0-9的数字,下划线_,美元符号$
  • 不能使用Java的关键字(包含保留字)和特殊值
  • 不能以数字开头
  • 不能包含空格
  • 严格区分大小写

标识符命名规范

  • 见名知意
  • 类名、接口名等:每个单词的首字母都大写,形式:XxxYyyZzz,
  • 变量、方法名等:从第二个单词开始首字母大写,其余字母小写,形式:xxxYyyZzz,
  • 包名等:每一个单词都小写,单词之间使用点.分割,形式:xxx.yyy.zzz,
  • 常量名等:每一个单词都大写,单词之间使用下划线_分割,形式:XXX_YYY_ZZZ

数据类型

在Java中,数据类型可以分为两种主要类型:原始数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)

原始数据类型

原始数据类型是Java语言中最基本的数据类型,用于表示简单的数据值。原始数据类型是值类型,直接存储实际的数据值,而不是对象的引用。Java的原始数据类型包括:

数据类型(Data Type) 描述(Description) 大小和范围(Size and Range) 示例(Example)
byte 用于表示整数值 8 位(8 bits) byte num = 100;
short 用于表示整数值 16 位(16 bits) short num = 5000;
int 用于表示整数值 32 位(32 bits) int num = 100000;
long 用于表示整数值 64 位(64 bits) long num = 1234567890L;
float 用于表示浮点数值 32 位(32 bits) float num = 3.14f;
double 用于表示浮点数值 64 位(64 bits) double num = 3.14159;
char 用于表示单个字符,采用 Unicode 编码 16 位(16 bits) char ch = ‘A’;
boolean 用于表示真或假值 不固定大小 boolean flag = true;

引用数据类型

引用数据类型用于表示对象的引用(内存地址),而不直接存储实际的数据值。这些数据类型是在类、接口或数组的基础上创建的。Java中的引用数据类型包括:

数据类型(Data Type) 描述(Description) 示例(Example)
类(Class) 用于创建对象的模板,实例化后成为对象。 class Person { … }
接口(Interface) 用于定义方法规范,类可以实现一个或多个接口。 interface Drawable { … }
数组(Array) 用于存储一组相同类型的数据。 int[] numbers = {1, 2, 3, 4, 5};

常量

在Java中,常量是指在程序执行过程中其值不能被改变的变量。常量在程序中起到固定值的作用,通常用来保存不可更改的值,如数值、字符串、布尔值等。Java中有两种类型的常量:字面常量(Literal Constants)和 final 常量(Final Constants)。

字面常量(Literal Constants)

字面常量是直接在代码中硬编码的值。例如:

类型(Type) 描述(Description) 示例(Example)
整数常量 用于表示整数值,不包含小数点或指数部分。 int x = 10;
浮点数常量 用于表示带有小数点或指数部分的数值。 double pi = 3.14159;
字符常量 用于表示单个字符。 char ch = 'A';
字符串常量 用于表示一串字符。 String message = "Hello, World!";
布尔常量 用于表示真或假值。 boolean isTrue = true;

final 常量(Final Constants)

在Java中,可以使用 final 关键字来声明常量,该常量一旦初始化后,其值就不能再改变。final 常量通常在类级别(static final)或对象级别声明。例如:

public static final double PI = 3.14159;

变量

在Java中,变量是用于存储和表示数据的命名内存位置。变量在程序中可以被赋予不同的值,并且可以在程序的执行过程中改变其值。在Java中,变量需要先声明,然后才能使用。

变量声明

在使用变量之前,需要先声明它们。变量声明包括变量的类型和名称。例如:

int age;  // 声明一个整数类型的变量age
double salary; // 声明一个浮点数类型的变量salary
String name; // 声明一个字符串类型的变量name

变量赋值

在声明变量之后,可以为其赋值。赋值操作使用赋值运算符(等号),将一个值存储到变量中。例如:

age = 25;  // 将25赋值给age变量
salary = 5000.50; // 将5000.50赋值给salary变量
name = "John Doe"; // 将"John Doe"赋值给name变量

变量命名规则

  • 变量名由字母、数字、下划线和美元符号组成。
  • 变量名必须以字母、下划线或美元符号开头,不能以数字开头。
  • 变量名区分大小写。
  • 变量名不能是Java的关键字(如int、double、if等)。
  • 建议使用具有描述性的变量名,以增加代码的可读性和可维护性。

变量类型

Java是一种静态类型语言,每个变量都必须有一个明确的类型。常见的变量类型包括:

类型 描述 示例
整数类型(Integer Types) 用于表示整数值,包括intshortlong等。 int age = 30;
浮点数类型(Floating-Point Types) 用于表示浮点数值,包括doublefloat等。 double pi = 3.14159;
字符类型(Character Type) 用于表示单个字符,是char类型。 char ch = 'A';
布尔类型(Boolean Type) 用于表示真或假值,是boolean类型。 boolean isTrue = true;
引用类型(Reference Types) 用于表示对象的引用,如StringListObject等。 String message = "Hello, World!";

变量作用域

变量的作用域是指变量在程序中可访问的范围。在Java中,变量的作用域可以是:

作用域 描述 示例
方法级别 在方法内部声明的变量,只能在该方法内部访问。 public void calculateSum() { int x = 10; }
类级别 在类内部、方法外部声明的变量,可以在整个类中访问。 public class MyClass { int age; }
块级别 在代码块(如循环、条件语句块)内部声明的变量,只能在该块内部访问。 for (int i = 0; i < 5; i++) { ... }

数组

数组(Array)是一种用于存储多个相同类型元素的数据结构,在Java中是一种重要的数据类型。数组在内存中以连续的方式存储元素,并可以通过索引访问数组中的每个元素。Java中的数组具有固定长度,一旦创建,其长度无法更改。

数据数据类型

数据类型(Data Type) 示例(Examples)
整数(int) int[] numbers;
双精度浮点数(double) double[] decimals;
字符(char) char[] characters;
布尔(boolean) boolean[] flags;
字符串(String) String[] names;
自定义类(Object) Person[] people;
二维数组(2D Array) int[][] matrix;
三维数组(3D Array) char[][][] cube;
动态初始化(Dynamic Initialization) int[] scores = new int[length];
包装类(Wrapper Class) Integer[] integerArray;
数组引用(Array Reference) int[] arrayRef;

声明数组

声明数组时需要指定数组的类型和名称,但不分配内存空间。语法:

dataType[] arrayName; // 例如:int[] numbers;

初始化数组

静态初始化:在声明的同时为数组分配内存空间并初始化元素。语法:

dataType[] arrayName = {element1, element2, ..., elementN}; // 例如:int[] numbers = {1, 2, 3, 4, 5};

动态初始化:先声明数组,然后使用new关键字为数组分配内存空间,并逐个或使用循环初始化元素。语法:

dataType[index] = value; // 例如:numbers[0] = 1;

代码示例

public class InputExample {
public static void main(String[] args) {
int[] numbers = {1,2,3,4,5};
for(int i=0;i<numbers.length;i++){
System.out.println(numbers[i]);
}
}
}

java.util.Arrays类内置方法

方法 描述
toString(arr) 将数组转换成字符串并返回。
sort(arr) 对数组进行排序。
binarySearch(arr, key) 在排序后的数组中使用二分查找定位指定元素。
equals(arr1, arr2) 比较两个数组是否相等。
fill(arr, value) 将数组中的所有元素都设置为指定的值。

toString(arr): 将数组转换为字符串并返回。该方法返回数组的字符串表示形式,其中元素用逗号分隔,并且包含在方括号中。

import java.util.Arrays;
public class InputExample {
public static void main(String[] args) {
int[] numbers = {1,2,3,4,5};
String string = Arrays.toString(numbers);
System.out.println(string);
}
}

sort(arr): 对数组进行升序排序,需要先进行排序然后通过toString方法将数组转换打印出来

import java.util.Arrays;
public class InputExample {
public static void main(String[] args) {
int[] numbers = {4,3,2,1,5};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers));
}
}

equals(arr1, arr2): 比较两个数组是否相等。如果两个数组拥有相同的长度,并且每个索引位置上的元素都相同,则认为这两个数组相等。

import java.util.Arrays;
public class InputExample {
public static void main(String[] args) {
int[] arr1 = {1,2,3};
int[] arr2 = {1,2,3};
boolean isEqual = Arrays.equals(arr1,arr2);
System.out.println(isEqual);
}
}

运算符

在Java中,运算符是用于执行各种操作的特殊符号。它们允许我们在变量、常量和表达式之间进行各种数学、逻辑和位运算。

算术运算符

运算符(Operator) 描述(Description) 示例(Example)
+ 加法运算符,用于执行加法操作。 int sum = 5 + 3;
- 减法运算符,用于执行减法操作。 int difference = 8 - 3;
* 乘法运算符,用于执行乘法操作。 int product = 4 * 6;
/ 除法运算符,用于执行除法操作。 double result = 10.0 / 3.0;
% 取模运算符,用于获取除法的余数。 int remainder = 10 % 3;

关系运算符

运算符(Operator) 描述(Description) 示例(Example)
== 等于运算符,检查两个操作数是否相等。 boolean isEqual = (x == y);
!= 不等于运算符,检查两个操作数是否不相等。 boolean notEqual = (a != b);
> 大于运算符,检查左操作数是否大于右操作数。 boolean isGreater = (age > 18);
< 小于运算符,检查左操作数是否小于右操作数。 boolean isLess = (price < maxPrice);
>= 大于等于运算符,检查左操作数是否大于等于右操作数。 boolean isGreaterOrEqual = (x >= y);
<= 小于等于运算符,检查左操作数是否小于等于右操作数。 boolean isLessOrEqual = (a <= b);

逻辑运算符

运算符(Operator) 描述(Description) 示例(Example)
&& 逻辑与运算符,当且仅当两个操作数都为true时,结果为true。 boolean result = (x > 0) && (y < 10);
` `
! 逻辑非运算符,用于对操作数进行取反操作,即true变为false,false变为true。 boolean isTrue = !(x > 10);

位运算符

运算符(Operator) 描述(Description) 示例(Example)
& 位与运算符,对操作数进行按位与操作。 int result = num1 & num2;
` ` 位或运算符,对操作数进行按位或操作。
^ 位异或运算符,对操作数进行按位异或操作。 int result = num1 ^ num2;
~ 位非运算符,对操作数进行按位取反操作。 int result = ~num;
<< 左移运算符,将操作数的所有位向左移动指定的位数。 int result = num << 2;
>> 右移运算符,将操作数的所有位向右移动指定的位数。 int result = num >> 1;
>>> 无符号右移运算符,将操作数的所有位向右移动指定的位数,空出的位用0填充。 int result = num >>> 3;

赋值运算符

运算符(Operator) 描述(Description) 示例(Example)
= 简单的赋值运算符,将右操作数的值赋给左操作数。 int x = 10;
+= 加法赋值运算符,将右操作数与左操作数进行加法运算,并将结果赋给左操作数。 x += 5; // 等同于 x = x + 5;
-= 减法赋值运算符,将右操作数与左操作数进行减法运算,并将结果赋给左操作数。 x -= 3; // 等同于 x = x - 3;
*= 乘法赋值运算符,将右操作数与左操作数进行乘法运算,并将结果赋给左操作数。 x *= 2; // 等同于 x = x * 2;
/= 除法赋值运算符,将右操作数与左操作数进行除法运算,并将结果赋给左操作数。 x /= 4; // 等同于 x = x / 4;
%= 取模赋值运算符,将右操作数与左操作数进行取模运算,并将结果赋给左操作数。 x %= 3; // 等同于 x = x % 3;

三元运算符

运算符(Operator) 描述(Description) 示例(Example)
条件 ? 表达式1 : 表达式2 三元运算符,根据条件的真假选择执行表达式1或表达式2。 int max = (a > b) ? a : b;

标点符号

在Java中,标点符号用于对代码进行结构化和组织,以及表示语句的结束和分隔。以下是Java中常见的标点符号及其详细解释:

标点符号 描述 示例
; 分号,用于表示语句的结束。 int x = 10;
{} 大括号,用于定义代码块,如类的主体、方法的主体和控制语句的主体。 java class MyClass { // 类的主体开始 ... // 方法的主体开始 ... // 控制语句的主体开始 ... }
() 小括号,通常用于表示方法的参数列表、表达式的优先级和控制语句的条件。 public void printNumber(int num) { ... }
[] 方括号,通常用于表示数组类型和访问数组元素。 int[] numbers = {1, 2, 3}; int value = numbers[2];
, 逗号,用于分隔数组元素、方法参数和枚举常量等。 int[] numbers = {1, 2, 3}; void printNumbers(int a, int b) { ... } enum Day { MONDAY, TUESDAY, ... }
. 点号,用于访问类的成员,如类的属性和方法。 String str = "Hello"; int length = str.length();

输入输出

换行输出

代码示例:

public class ForLoopExample {
public static void main(String[] args) {
String name = "acai";
int age = 21;
System.out.println(name);
System.out.println(age);
}
}

结果:

acai
21

不换行输出

代码示例:

public class ForLoopExample {
public static void main(String[] args) {
String name = "acai";
int age = 21;
System.out.print(name);
System.out.print(age);
}
}

结果:

acai21

format格式化字符串

String.format() 方法允许你使用格式说明符来格式化输出。格式说明符以 % 开头,后面跟着表示数据类型的字符。常见的格式说明符:

格式说明符 数据类型
%d 整数
%f 浮点数
%s 字符串
%c 字符
%b 布尔值
%n 换行符

示例代码:

public class ForLoopExample {
public static void main(String[] args) {
int num = 42;
String text = "Java";
System.out.printf("数字:%d,编程语言:%s", num, text);
}
}

运行结果:

数字:42,编程语言:Java

DecimalFormat格式化数字

DecimalFormat 类允许你使用特定的模式来格式化数字。这在你想要控制小数位数或添加千位分隔符时非常有用。示例代码:

import java.text.DecimalFormat;
public class ForLoopExample {
public static void main(String[] args) {
double amount = 12345.6789;
DecimalFormat df = new DecimalFormat("#,###.##");
String formattedAmount = df.format(amount);
System.out.println("格式化后的金额:" + formattedAmount);

}
}

运行结果:

格式化后的金额:12,345.68

输入

在Java中,获取输入通常是通过用户在控制台输入数据或通过其他外部来源(如文件、网络等)读取数据。对于控制台输入,使用 Scanner 类。Scanner 是 Java 标准库中的一个类,可以用来获取用户在控制台输入的各种数据类型。首先,你需要在代码中导入 java.util.Scanner 包。
下面是 java.util.Scanner 类中一些常用的内置方法:

方法签名 描述
Scanner(InputStream source) 构造一个新的 Scanner 对象来扫描指定的输入流。
Scanner(String source) 构造一个新的 Scanner 对象来扫描指定的字符串。
boolean hasNext() 如果输入中还有另一个标记(例如,另一个单词或整数),则返回 true。
boolean hasNextInt() 如果输入中还有另一个整数,则返回 true。
boolean hasNextDouble() 如果输入中还有另一个双精度浮点数,则返回 true。
boolean hasNextLine() 如果输入中还有另一行,则返回 true。
String next() 获取输入中的下一个标记(例如,下一个单词或整数)。
int nextInt() 解析输入中的下一个标记为整数,并返回该整数。
double nextDouble() 解析输入中的下一个标记为双精度浮点数,并返回该浮点数。
String nextLine() 获取输入中的下一行内容,并将扫描器位置移到下一行。
boolean hasNextBoolean() 如果输入中还有另一个布尔值,则返回 true。
boolean nextBoolean() 解析输入中的下一个标记为布尔值,并返回该布尔值。
void close() 关闭此扫描器。

获取整数

import java.util.Scanner;

public class InputExample {
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个整数:");
int number = scanner.nextInt();
System.out.println("你输入的整数为: " + number);
// 释放资源
scanner.close();
}
}

获取字符串

import java.util.Scanner;

public class InputExample {
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个整数:");
String number = scanner.next();
System.out.println("你输入的字符串为: " + number);
// 释放资源
scanner.close();
}
}

获取布尔值

import java.util.Scanner;

public class InputExample {
public static void main(String[] args){
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个布尔值:");
Boolean number = scanner.nextBoolean();
System.out.println("你输入的布尔值为: " + number);
// 释放资源
scanner.close();
}
}

流程控制

  • break: 结束循环
  • continue:跳过本次循环

if else判断

语法:

if(条件表达式){
代码语句
}else {
代码语句
}

示例代码:

import java.util.Scanner;

public class InputExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("输入第一个数字:");
int int1 = scanner.nextInt();
System.out.print("请输入第二个数字:");
int int2 = scanner.nextInt();
if (int1 > int2) {
System.out.print("第一个数字大于第二个数字");
}else {
System.out.print("第一个数字小于第二个数字");
}
}
}

switch case循环

语法

switch (expression) {
case value1:
// 当 expression 的值等于 value1 时执行的代码
break;
case value2:
// 当 expression 的值等于 value2 时执行的代码
break;
// 可以有更多的 case 分支
default:
// 当 expression 的值与之前所有的 case 都不匹配时执行的代码
break;
}

示例代码

public class InputExample {
public static void main(String[] args) {
int dayOfWeek = 2;

switch (dayOfWeek) {
case 1:
System.out.println("星期一");
break;
case 2:
System.out.println("星期二");
break;
case 3:
System.out.println("星期三");
break;
case 4:
System.out.println("星期四");
break;
case 5:
System.out.println("星期五");
break;
default:
System.out.println("周末");
}
}
}

for循环

语法

for(初始化语句①; 循环条件语句②; 迭代语句④){
循环体语句③
}

代码示例:

public class InputExample {
public static void main(String[] args) {
for(int i=1;i<=5;i++){
System.out.println(i);
}
}
}

while循环

语法

while (循环条件语句①) {
循环体语句②;
}

代码示例

public class InputExample {
public static void main(String[] args) {
int num = 1;
while (num != 0) {
System.out.println(num);
num -= 1;
}
}
}

do while循环

do-while 是Java中的一种循环控制结构,它允许您在指定条件为真时重复执行一段代码块。do-while 和 while 循环之间的主要区别在于,do-while循环先执行代码逻辑再判断判断条件。

语法

do {
// 需要执行的代码块
} while (条件);

代码示例

public class InputExample {
public static void main(String[] args) {
int num = 1;
do {
System.out.println(num);
num += 1;
} while (num < 5);
}
}

Java模块

内置模块

System

方法 描述
getProperty 获取系统属性或环境变量,该方法接受一个String类型的参数.例如:System.getProperty("os.name")

面向对象

  • 面向过程的程序设计思想(Process-Oriented Programming),简称POP。过程就是操作数据的步骤,如果某个过程的实现代码在很多地方重复出现,那么就可以把这个过程抽象为一个函数,这样就可以大大简化冗余代码,也便于维护。代码结构:以函数为组织单位。独立于函数之外的数据称为全局数据,在函数内部的称为局部数据。
  • 面向对象的程序设计思想( Object Oriented Programming),简称OOP。面向对象思想就是在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。某个事物的一个具体个体称为实例或对象。代码结构:以类为组织单位。每种事物都具备自己的属性(即表示和存储数据,在类中用成员变量表示)和行为/功能(即操作数据,在类中用成员方法表示)。

类和对象

什么是类

类是一种自定义的数据类型,用于定义对象的属性和行为。它是对象的模板,描述了对象有哪些属性(也称为成员变量)和能够执行哪些操作(也称为成员方法)。类通常由关键字”class”声明,类名的首字母通常要大写。
示例代码:class Car就是定义的一个类,start()和stop()是类的方法

public class Car {
// 行为(成员方法)
void start() {
System.out.println("The car is starting.");
}

void stop() {
System.out.println("The car is stopping.");
}
}

什么是对象

对象是类的一个实例,具体表示了类所描述的实体。通过使用关键字”new”来创建一个对象。创建对象后,可以使用对象的方法和访问其属性。
示例代码:Car myCar = new Car();将上面的Car类创建了一个类的对象,通过创建的对象调用类的方法

public class Car {
void start(){
System.out.println("The car is Starting");
}
void stop(){
System.out.println("The car is Stopped");
}
public static void main(String[] args){
Car myCar = new Car();
myCar.start();
myCar.stop();
}
}

类和对象的关系

  1. 类是模板,对象是实例: 类是一种自定义的数据类型,它描述了对象的属性和行为。类可以看作是对象的蓝图或模板。它定义了对象应该有哪些属性和方法。而对象是类的一个实例,表示了类所描述的具体实体。类是对象的定义,而对象是类的实现。
  2. 类定义行为和属性,对象执行行为和拥有属性: 在类中,我们可以定义方法(行为)和属性(变量)。方法是描述对象能够做什么的函数,属性是描述对象状态的变量。当我们创建一个类的对象时,该对象会拥有该类中定义的属性,并且可以执行该类中定义的方法。
  3. 类是抽象的,对象是具体的: 类是抽象的概念,它定义了一类对象共同的特征和行为。它并不存在于内存中。而对象是具体的实例,它占据了内存空间并可以在程序中被操作。
  4. 多个对象可以属于同一个类: 一个类可以用来创建多个对象。例如,我们可以有一个”Car”类,然后创建多个不同的汽车对象,每个对象都具有该类所描述的属性和行为。

如何定义类

名称 描述
类名 类名用于标识类的名称。根据Java的命名规范,类名的首字母通常要大写。
成员变量 成员变量是类的属性,它们用于描述对象的状态。这些变量可以是任意的合法数据类型(例如int、String、自定义类等)。成员变量在类中声明,但不直接在方法中使用。
构造方法 构造方法是一种特殊的方法,用于在创建对象时对对象进行初始化。构造方法的名称必须与类名相同。它没有返回类型,包括void关键字。当使用new关键字创建类的对象时,将调用构造方法来初始化对象。
成员方法 成员方法是类的行为,它们定义了对象可以执行的操作。方法可以有返回值(即returnType),也可以是void类型,表示不返回任何值。方法的参数(如果有)用于接收传递给方法的数据。

示例代码:

// 使用关键字"class"定义一个类
public class ClassName {
// 成员变量(属性)
dataType variable1;
dataType variable2;
// ... 可以定义更多的成员变量

// 构造方法(Constructor)
public ClassName(parameters) {
// 构造方法的逻辑,用于初始化对象
}

// 成员方法(行为)
returnType methodName(parameters) {
// 方法的逻辑
// 可以访问成员变量,并执行一些操作
return value; // 如果有返回值的话
}

// ... 可以定义更多的成员方法
}
  1. public class ClassName,表示定义一个公共类,类名称为ClassName
  2. dataType variable1;,表示类属性,比如可以定义一个整数类属性数据:int age
  3. public ClassName(parameters),表示类的构造方法当创建类对象的时候用来初始化对象。比如:创建类对象的时候会自动调用构造方法
  4. returnType methodName(parameters),表示成员方法returnType是返回值的类型如果该方法没有返回值则returnTypevoid

如何创建类对象

创建类对象的语法

类名 对象名 = new 类名();

示例:

Car myCar = new Car();

在Java中,包(Package)是一种用于组织类和接口的机制。它是一种命名空间,用于在Java程序中对类和接口进行逻辑上的分组和管理。包有助于防止命名冲突,使得Java代码更加有条理和易于维护。

包的作用

  • 防止命名冲突:通过使用不同的包名,可以确保类和接口的名称在不同的包中不会冲突。
  • 组织代码:包可以将相关的类和接口组织在一起,使得代码更加有条理和易于维护。
  • 访问控制:包可以定义访问权限,有助于控制类和接口在其他包中的可见性。
  • 类的导入:通过使用import语句,可以在其他类中方便地引用不同包中的类和接口。

包的定义

在Java中,一个包是一个包含类和接口的目录。每个包对应一个文件夹,在文件夹中存放相关的Java类文件。包的名称是由一系列用点.分隔的标识符组成,类似于文件路径。例如:com.example.mypackage

包的声明

在Java源代码文件中,包的声明必须是文件的第一个非注释性语句。包的声明使用package关键字,后面跟着包名。例如:package com.example.mypackage;。 没有显式包声明的类将被放置在默认包中,建议避免使用默认包,而是为每个类指定明确的包。

包的导入

在使用其他包中的类和接口时,可以使用import语句将类引入到当前的Java文件中。例如:import java.util.ArrayList;
使用通配符*可以导入整个包的所有类。例如:import java.util.*;

案例:如何使用包?

  1. 目录结构如下:
    ├── src
    │   ├── com
    │   │   └── blog
    │   │   └── mypackage
    │   │   └── MyClass.java
    │   └── main.java
  2. 编辑MyClass包vi src/com/blog/mypackage/MyClass.java
    package mypackage;
    public class MyClass {
    public void sayHello(){
    System.out.println("MyClass package");
    }
    }
  3. src/main.java代码中引入MyClass包,并调用该包内的sayHello方法
    import mypackage.MyClass;

    public class main {
    public static void main(String[] args){
    MyClass myclass = new MyClass();
    myclass.sayHello();
    }
    }

成员变量

成员变量(Member Variable),也称为实例变量(Instance Variable),是定义在类中的变量,属于类的一部分。成员变量通常用于描述一个类的属性或状态。它们保存对象的状态信息,并且在类的方法中可以被访问和操作。在Java中,成员变量定义在类的内部,但在方法的外部。它们通常声明为类的字段,可以带有不同的访问修饰符来控制其可见性和访问权限。

声明成员变量语法

[修饰符] class 类名{
[修饰符] 数据类型 成员变量名;
}

成员变量修饰符

修饰符 说明
public 公共访问修饰符,对所有类可见。
protected 受保护访问修饰符,对当前类及其子类可见。
default 包级私有访问修饰符,默认修饰符,对同一个包中的类可见。
private 私有访问修饰符,对当前类可见,其他类无法直接访问。

示例:定义成员变量

public class test {
public String name; // 公共访问修饰符,可以在其他类中直接访问
private int age;// 私有访问修饰符,只能在当前类中访问
protected String addr;// 受保护访问修饰符,可以在当前类和其子类中访问
}

对象实例变量

在Java中,对象实例变量是类的成员变量,也称为实例变量或非静态变量。这些变量与类的实例(对象)相关联,每个对象都有自己的一组实例变量,它们的值可以因对象而异。实例变量在类的声明中定义,而不是在静态(static)关键字修饰的方法或代码块中。

声明和访问

在类的定义中,使用合适的访问修饰符(例如:public、private、protected、或默认访问修饰符)以及数据类型来声明实例变量。这些变量在类的任何非静态方法中都可以访问,包括构造函数。
示例:

public class MyClass {
// 实例变量声明
public int age;
private String name;
// ...其他代码
}

生命周期

实例变量的生命周期与对象的生命周期相同。它们在对象创建时分配内存,在对象被垃圾收集器回收时释放内存。

访问控制

通过使用不同的访问修饰符,可以限制对实例变量的访问。通常,建议将实例变量声明为私有(private),并提供公共(public)方法(getters和setters)来访问和修改这些变量的值。

方法

方法也叫函数,是一组代码语句的封装,从而实现代码重用,从而减少冗余代码,通常它是一个独立功能的定义,方法是一个类中最基本的功能单元。

方法的特点

  • 封装性(Encapsulation): 方法允许将代码组织成独立的功能单元。通过封装代码在方法中,可以将复杂的操作隐藏在方法内部,只向外部提供简单的接口。这提高了代码的可维护性,使代码更易于理解和使用。
  • 代码重用(Reuse): 方法允许在程序中多次调用相同的代码块。通过定义方法一次,可以在不同的地方多次调用它,从而避免重复编写相同的代码。这种代码重用提高了代码的效率和可维护性。
  • 模块化(Modularity): 方法允许将大型程序分解为小的、相互独立的模块。每个方法负责实现一个特定的功能,通过组合这些方法来构建复杂的程序。这种模块化使程序的开发和维护更加简单。
  • 参数传递(Parameter Passing): 方法可以接受输入参数,这些参数用于在方法内部执行操作。通过参数传递,方法可以接受外部数据并进行处理,使得方法的行为可以根据传递的参数值动态变化。
  • 返回值(Return Value): 方法可以返回一个值作为其执行结果。通过返回值,方法可以将计算结果或处理结果传递给调用方。返回值使得方法可以在调用处继续使用和处理方法的结果。
  • 抽象性(Abstraction): 方法提供了对操作的抽象描述,而不需要了解其内部实现细节。调用方只需知道方法的名称、参数和返回类型,而不需要了解方法内部的具体实现。
  • 递归(Recursion): 方法可以调用自身,这被称为递归。递归是一种强大的编程技术,可以用来解决需要重复执行相似任务的问题,例如在树和图的遍历中。
  • 多态性(Polymorphism): 在面向对象编程中,多态性是指可以使用父类的引用来引用子类的对象。方法的多态性使得在运行时根据实际对象类型来调用适当的方法,而不需要显式地指定具体实现。
  • 动态绑定(Dynamic Binding): 在Java中,方法的调用在运行时进行动态绑定。这意味着方法的具体实现由调用方的对象类型决定,而不是方法调用的代码所在的位置。

声明方法

在Java中,声明方法需要指定方法的修饰符、返回类型、方法名、参数列表和可能的异常声明。方法的声明可以在类的内部,也就是类的成员之中。以下是声明方法的基本语法:

[修饰符] 返回类型 方法名(参数列表) [throws 异常列表] {
// 方法体,执行操作和逻辑
// 如果返回类型为void,则可以省略return语句,或者使用"return;"来表示方法结束
// 如果返回类型不为void,则必须使用"return 值;"语句返回结果
}

解析:

  1. 修饰符(Modifiers): 修饰符是可选的,用于控制方法的访问权限和其他特性。常用的修饰符包括:public、private、protected、static、final等。如果不写修饰符,默认是包访问权限。
  2. 返回类型(Return Type): 返回类型是方法执行后返回的数据类型。如果方法不返回任何值,则使用void关键字作为返回类型。如果返回值是其他数据类型,则需要指定具体的返回类型。
  3. 方法名(Method Name): 方法名是用来标识方法的唯一名称。它应该是一个合法的Java标识符,并且遵循命名规范。
  4. 参数列表(Parameter List): 方法可以带有零个或多个参数,多个参数之间用逗号分隔。每个参数由参数类型和参数名组成。参数用于传递数据给方法,供方法在执行时使用。
  5. 异常声明(Exception Declaration): 异常声明是可选的,用于声明方法可能抛出的异常类型。如果方法可能抛出已检查异常(checked exception),则需要在方法声明中使用throws关键字声明这些异常。

调用方法

在Java中,调用实例方法需要先创建类的对象,然后使用该对象来调用方法。实例方法是与对象实例相关联的,因此只能通过对象来调用。代码示例:

// test.java
public class test {
// 定义sayHello方法
public void sayHello() {
System.out.println("sayHello");
}
// 定义main方法,程序会默认执行main方法下的代码也是代码入口
public static void main(String[] args){
// 实例化test类,实例化的对象为obj
test obj = new test();
// 通过obj访问sayHello方法
obj.sayHello();
}
}

运行代码:

// 先编译test.java
javac test.java
// 运行test
java test

形参和实参

  • 形参:是在方法定义中声明的参数,用于接收传递给方法的数据。
  • 实参:是在方法调用时传递给方法的具体数据或值。

示例代码:

public class test {
// num1 num2 为形参
public int addnum(int num1, int num2) {
return num1 + num2;
}

public static void main(String[] args) {
test obj = new test();
// 1,2 为实参
int result = obj.addnum(1, 2);

String formatstr = String.format("result: %d%n", result);
System.out.print(formatstr);
}
}

返回值

返回值是指在方法执行后,从方法中返回给调用方的结果或数据。在Java中,方法可以有返回值,也可以没有。返回值类型在方法声明时指定,如果方法没有返回值,则返回类型为void。

返回值类型

方法的返回值类型是指方法返回的数据类型。如果方法有返回值,就必须在方法声明中指定返回值类型,可以是Java的基本数据类型(如int、double、boolean等),也可以是对象类型。
示例代码:

public class test {
// 返回整型数据
public int intFun(){
return 1;
}
// 返回字符串数据
public String strFun(){
return "test";
}
}

返回值的作用

通过返回值,方法可以将计算的结果、处理的数据或其他信息传递给调用方,实现方法之间的数据交互和结果传递。

方法调用对象成员

在Java中,this是一个关键字,它是一个引用,指向当前对象的实例。this关键字通常用于在类的方法内部引用当前对象的成员变量和方法。它允许我们在方法内部区分实例变量和局部变量之间的冲突,同时也可以在方法内部调用其他实例方法。
示例代码:

public class test {
public int number = 1;

// 构造方法,与类名一致,没有返回类型
public test() {
int num = this.number;
System.out.println(num);
}

public static void main(String[] args) {
test obj = new test();
}
}

可变参数

可变参数(Variable Arguments)是Java中一种特殊的方法参数类型,允许方法接受可变数量的参数,而无需显式地指定每个参数。这在需要处理不定数量的参数时非常方便。在Java中,可变参数通过三个点(…)来表示。可变参数的格式:

【修饰符】 返回值类型 方法名(【非可变参数部分的形参列表,】参数类型... 形参名){  }

可变参数可接受的数据类型:

数据类型 描述
基本数据类型 可变参数可以接受Java的基本数据类型,例如:int、double、float、char、byte、short、long和boolean。
对象类型 可变参数也可以接受Java的对象类型,包括用户自定义的类、标准库提供的类和其他所有类的实例。
数组 可变参数可以接受数组作为参数。数组本身被视为一个对象,因此可以作为可变参数传递给方法。

示例代码:

public class test {
// 定义可变参数number
public void printNums(int... number){
for (int num : number) {
System.out.println(num);
}
}
public static void main(String[] args) {
test obj = new test();
// 实参调用类方法
obj.printNums(0,1,2,3);
}
}

命令行参数

命令行参数是指在运行Java程序时,在命令行中传递给程序的参数。这些参数可以用于向程序传递配置信息、输入数据、标志等。在Java中,命令行参数可以通过main方法的args参数列表来接收。
示例代码:

import java.util.Arrays;
public class test {
public static void main(String[] args) {
System.out.println(Arrays.toString(args));
}
}

执行代码:

// 执行命令
java test 1 2 3 4
// 输出结果
[1, 2, 3, 4]

方法的重载

指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可,与修饰符和返回值类型无关。比如:在同一个类中定义三个不同的方法,这三个方法形参的数量不同。java在调用方法的时候会根据实参的数量匹配对应的方法。示例代码:

public class test {
public void result(int num){
System.out.printf("int: %d%n", num);
}
public void result(String num){
System.out.printf("string: %s%n", num);
}
public static void main(String[] args){
test obj = new test();
obj.result(1);
obj.result("test");
}
}

执行结果:

// 当调用result方法时传递参数为int,则调用第一个方法;当传递参数为string时,则调用第二个方法。
int: 1
string: test

方法的递归

方法递归是指在一个方法的定义中调用该方法本身的过程。这种调用方式可以将复杂的问题拆解成更小的子问题,使得解决问题的过程更加简单和直观。在递归中,方法会反复调用自己,直到满足某个特定的条件才会停止递归。

递归调用

在方法的定义中,可以通过方法名来调用该方法,这称为方法的递归调用。递归调用可以是直接递归(即方法直接调用自身),也可以是间接递归(即方法 A 调用方法 B,而方法 B 又调用方法 A)。
示例代码:

public class test {
public static int Test(int n) {
if (n < 10) {
return n;
} else {
return Test(n + 1);
}
}

public static void main(String[] args) {
int result = Test(1);
System.out.println("Result: " + result);
}
}

封装

将类的数据(属性)和方法(行为)组合成一个独立的单元,并对外部隐藏类的内部实现细节,只暴露必要的接口供其他类进行交互。封装提供了数据的安全性和代码的模块化,同时也降低了类与类之间的耦合性。

  1. 数据隐藏: 封装的主要目的是隐藏类的内部数据细节。通过将数据声明为私有(private),外部类无法直接访问和修改这些数据,只能通过类提供的公共方法(getters和setters)来访问和修改数据。这样可以防止外部类直接修改类的数据,确保数据的安全性和完整性。
  2. 访问控制符: 在Java中,类的属性和方法可以使用访问控制符来指定其可见性。常见的访问控制符有:private、public、protected和包访问权限(默认)。private修饰的属性和方法只能在类的内部访问,public修饰的属性和方法可以在任何地方访问,protected修饰的属性和方法可以在子类和同一包内访问,包访问权限则是在同一包内可以访问。
  3. 成员变量的封装: 在类中,将成员变量声明为private,然后通过公共的getter和setter方法来访问和修改这些成员变量,以实现对成员变量的封装。
  4. 方法的封装: 在类中,除了对成员变量进行封装外,还可以对方法进行封装。通过将方法声明为private,可以确保该方法只能在类的内部被调用,而不对外部暴露实现细节。
  5. 优点:
    • 提高了代码的安全性和可维护性,防止了数据被非法修改。
    • 通过公共接口,隐藏了类的内部实现细节,减少了类与类之间的耦合,使代码更易于理解和修改。
    • 改变类的内部实现时,不会影响其他类的使用,提供了良好的代码封装和抽象。

封装成员变量

在类中,将成员变量声明为private,然后通过公共的getter和setter方法来访问和修改这些成员变量,以实现对成员变量的封装。

实现封装成员变量的步骤

  1. 使用 private 修饰成员变量
    语法:
    private 数据类型 变量名 ; 
    示例代码:
    // src/main/java/org/example/Person.java
    package org.example;
    public class Person {
    private String name;
    private int age;
    public void setName(String str){
    name = str;
    }
    public void setAge(int agea){
    age = agea;
    }
    public String getName(){
    return name;
    }
    public int getAge(){
    return age;
    }
    }
  2. 测试,通过ide创建maven项目。目录结构如下:
    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   ├── org
    │   │   │   │   └── example
    │   │   │   │   ├── Main.java
    │   │   │   │   ├── Person.class
    │   │   │   │   └── Person.java
    │   │   │   └── test.java
    │   │   └── resources
    代码示例:
    // src/main/java/test.java
    import org.example.Person;
    public class test {
    public static void main(String[] args){
    Person obj = new Person();
    obj.setName("acai");
    System.out.println(obj.getName());
    obj.setAge(28);
    System.out.println(obj.getAge());
    }
    }

继承

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成某种关系。其中,多个类可以称为子类,也叫派生类;多个类抽取出来的这个类称为父类、超类(superclass)或者基类。

继承的优势

  • 提高代码的复用性
  • 提高代码的扩展性

继承的特性

通过继承,子类会自动拥有父类的非私有成员(字段和方法),包括公有(public)和受保护(protected)访问修饰符的成员。私有(private)成员不能被继承。

基本语法

[修饰符] class 父类 {
...
}

[修饰符] class 子类 extends 父类 {
...
}

继承示例

  1. 父类
    // src/main/java/org/example/Animal.java
    package org.example;

    public class Animal {
    String name;
    int age;
    public void eat(){
    System.out.println("name:" + name + " age:" + age);
    }
    }
  2. 子类
    // src/main/java/org/example/Cat.java
    package org.example;
    import org.example.Animal;

    public class Cat extends Animal {
    public static void main(String[] args){
    Cat obj = new Cat();
    obj.name = "acai";
    obj.age = 27;
    obj.eat();
    }
    }

继承的特点

  1. 子类会继承父类的所有实例变量和实例方法
    从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
  2. java只支持单继承不支持多继承
    public class A{}
    class B extends A{}

    //一个类只能有一个父类,不可以有多个直接父类。
    class C extends B{} //ok
    class C extends A,B... //error
  3. java支持多层继承
    class A{}
    class B extends A{}
    class C extends B{}
  4. 一个父类可以拥有多个子类
    class A{}
    class B extends A{}
    class D extends A{}
    class E extends A{}

权限修饰符

访问修饰符 (Access Modifier) 继承情况 (Inheritance) 子类中的访问权限 (Access in Subclass)
public 子类可以继承父类的public成员,并且在子类中可以直接访问。 子类中可以直接访问继承的public成员。
protected 子类可以继承父类的protected成员,并且在子类中可以直接访问。 对于不同包中的子类,只有当父类和子类在同一个包中,或者子类继承了父类之后,才能访问继承的protected成员。
default (包级访问修饰符) 如果父类和子类在同一个包中,子类可以继承父类的default成员,并且在子类中可以直接访问。 对于不同包中的子类,无法访问继承的default成员。
private 子类无法继承父类的private成员,因此无法直接访问。 无法访问父类的private成员。

父类私有成员变量

子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。示例代码:
父类:

public class Animal {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

子类:

public class Cat extends Animal {
public static void main(String[] args){
Cat catObj = new Cat();
catObj.setName("acai");
catObj.setAge(28);
System.out.println("Name: " + catObj.getName());
System.out.println("Age: " + catObj.getAge());
}
}

方法重写

方法重写(Method Overriding)是面向对象编程中的一种重要概念,它允许子类重新定义(覆盖)父类中具有相同名称、参数列表和返回类型的方法。重写方法的目的是在子类中提供一种特定的实现,从而覆盖父类中的默认行为。

方法重写规则

  1. 方法名、参数列表和返回类型必须与父类中被重写的方法相同。
  2. 访问修饰符(例如public、protected、private、或默认的包级访问)不能缩小子类中重写方法的访问权限。也就是说,子类中的重写方法的访问修饰符不能比父类中被重写方法的访问修饰符更严格。
  3. 子类中的重写方法不能抛出比父类中被重写方法更多的受检查异常(checked exception),但可以抛出更少或不抛出受检查异常,或者抛出父类方法抛出异常的子类异常。
  4. static方法和final方法不能被重写。static方法属于类本身,而不是实例,因此不存在多态性。final方法在父类中已经被终态化(不能再被重写),因此无法在子类中重写。

代码示例

父类:

package org.example;

public class Animal {
public void cat(){
System.out.println("阿才的博客");
}
}

子类:

package org.example;

public class Cat extends Animal {
public void cat(){
System.out.println("www.acaiblog.top");
}
public static void main(String[] args){
Cat obj = new Cat();
obj.cat();
}
}

多态

多态是面向对象编程中的一个重要概念,它指的是一个对象可以表现出多种形态的能力。具体来说,多态性允许使用通用的接口来处理不同类型的对象,而无需关心对象的具体类型,从而提高了代码的灵活性和可扩展性。
多态性的优势在于它可以让我们编写更灵活、更通用的代码。例如,假设有一个Shape父类和它的子类Circle和Rectangle。我们可以使用Shape引用来处理不同的图形对象,而不必为每种图形写不同的代码。示例代码:

package org.example;

class Shape{
public void draw(){
System.out.println("drawing a shape");
}
public static void main(String[] args){
Circle obj1 = new Circle();
Rectangle obj2 = new Rectangle();
obj1.draw();
obj2.draw();
}
}
class Circle extends Shape{
@Override // @Override代表该方法是一个重写方法
public void draw(){
System.out.println("drawing a shape");
}
}
class Rectangle extends Shape{
@Override
public void draw(){
System.out.println("drawing a Rectangle");
}
}

实例初始化

在Java中,实例初始化是指在创建对象时,对对象的成员变量进行初始化的过程。它是对象构造的一部分,确保对象在被使用之前具有合适的初始状态。实例初始化主要通过构造器(构造方法)来完成,但也可以使用实例初始化块来实现更复杂的初始化逻辑。

构造器

什么是构造器

在Java中,构造器(Constructor)是一种特殊的方法,用于创建对象并对其进行初始化。构造器的名称必须与类名完全相同,并且没有返回类型(甚至没有void)。每当使用new关键字创建一个新的对象时,构造器就会被调用。
构造器的主要作用是确保对象在创建时处于一个有效和一致的状态。它可以用于初始化对象的实例变量(成员变量),执行其他必要的设置,并在对象创建后执行一些特定的操作。
在Java中,如果没有显式地定义构造器,编译器会为类提供一个默认构造器。默认构造器没有参数,并执行最基本的初始化操作,例如将实例变量设置为默认值(数值为0,布尔为false,对象为null等)。

构造器的特点

  1. 构造器的名称必须与类名相同。
  2. 构造器没有返回类型,甚至没有void关键字。
  3. 可以定义多个构造器(重载构造器),只要它们的参数类型或个数不同,Java会根据传入的参数选择调用合适的构造器。
  4. 如果一个类没有定义任何构造器,编译器将提供一个默认的无参构造器。但如果类中定义了至少一个构造器,且没有无参构造器,则在创建对象时如果没有显式调用合适的构造器,编译将会报错。

构造器语法

语法:

[修饰符] class 类名{
[修饰符] 构造器名(){
// 实例初始化代码
}
[修饰符] 构造器名(参数列表){
// 实例初始化代码
}
}

构造器示例:

package org.example;

public class Main {
private String name;
private int age;

// 无参构造器
public Main(){
name = "acai";
age = 27;
System.out.println(name + " " + age);
}
// 有参构造器
public Main(String name, int age){
this.name = name;
this.age = age;
System.out.println(name + " " + age);
}
public static void main(String[] args){
Main obj1 = new Main();
Main obj2 = new Main("www.acaiblog.top",30);

}
}

构造器相互调用

在Java中,一个构造器可以调用同一个类中的其他构造器,这种调用称为构造器之间的相互调用,也被称为构造器重载。
构造器之间的相互调用通过使用this关键字来实现。this关键字表示当前对象的引用,可以在构造器内部使用它来调用其他构造器。需要注意的是,相互调用中只能有一个构造器是直接调用对象创建语句(例如new关键字),而其他构造器则间接调用到这个构造器。
构造器互相调用原则:

  • 直接调用:构造器可以直接调用类中的其他构造器,通过使用this()语法来实现。注意,直接调用语句必须在构造器的第一行,且一个构造器中最多只能有一次直接调用其他构造器的语句。
  • 间接调用:构造器可以通过相互调用链来间接调用其他构造器。例如,构造器A调用构造器B,构造器B再调用构造器C,依此类推。在这种情况下,间接调用的构造器链必须最终以一个直接调用对象创建语句(例如new关键字)结束。

示例代码:

package org.example;

public class Main {
private int value;

public Main(){
// this中的值是有参构造器value参数的值,通过实例化类的时候自动调用无参构造器,无参构造器又调用了有参构造器。相当于实例化对象的时候调用了有参构造器
this(1);
}

public Main(int value) {
this.value = value;
System.out.println(value);
}
public static void main(String[] args){
Main obj = new Main();
}
}

类继承构造器处理

  • 子类继承父类时,不会继承父类的构造器。只能通过super()或super(实参列表)的方式调用父类的构造器。
  • super();:子类构造器中一定会调用父类的构造器,默认调用父类的无参构造,super();可以省略。
  • super(实参列表);:如果父类没有无参构造或者有无参构造但是子类就是想要调用父类的有参构造,则必须使用super(实参列表);的语句。
  • super()和super(实参列表)都只能出现在子类构造器的首行

无参代码示例:

// 父类 src/main/java/org/example/Animal.java
package org.example;

public class Animal {
public Animal(){
System.out.println("test");
}
}
// 子类 src/main/java/org/example/Cat.java
package org.example;
import org.example.Animal;

public class Cat extends Animal{
public Cat(){
super();
}
public static void main(String[] args){
System.out.println("starting");
Cat obj = new Cat();
}
}

有参代码示例:

// 父类 src/main/java/org/example/Animal.java
package org.example;

public class Animal {
public Animal(String name){
System.out.println(name);
}
}
// 子类 src/main/java/org/example/Cat.java
package org.example;
import org.example.Animal;

public class Cat extends Animal{
public Cat(){
super("acai");
}
public static void main(String[] args){
System.out.println("starting");
Cat obj = new Cat();
}
}

关键字

native

在Java中,native是一个关键字,用于表示某个方法的实现是由外部的本地(Native)代码提供,而不是由Java代码实现。本地方法是指由其他编程语言(如C、C++)编写的方法,可以在Java中使用。

实现步骤

  1. 在Java代码中声明native方法:在Java代码中声明一个native方法,告诉编译器该方法的实现将在本地库中找到。
  2. 创建本地库:使用其他编程语言(如C、C++)实现声明的本地方法,然后编译为本地库文件。
  3. 加载本地库:在Java代码中使用System.loadLibrary()方法或System.load()方法加载本地库,使得Java虚拟机可以找到本地方法的实现。

代码示例

java代码:

public class TestNative {
public native void nativeMethod();

static {
System.loadLibrary("MyNativeLibrary"); // 加载本地库
}

public static void main(String[] args) {
TestNative test = new TestNative();
test.nativeMethod(); // 调用本地方法
}
}

本地库的C实现MyNativeLibrary.c:

#include <jni.h>

JNIEXPORT void JNICALL Java_TestNative_nativeMethod(JNIEnv *env, jobject obj) {
// 在这里实现本地方法的功能
printf("This is a native method!\n");
}

final

  1. 当一个类被声明为final时,表示该类是最终的,不能被继承。这意味着其他类不能扩展(继承)final类,它是一个不可扩展的类。例如:
    public final class MyClass {
    // 类的内容
    }
  2. 当一个方法被声明为final时,表示该方法是最终的,不能被子类重写(覆盖)。这样,子类无法改变父类final方法的行为。例如:
    public class MyClass {
    public final void myMethod() {
    // 方法的内容
    }
    }
  3. 当一个变量被声明为final时,表示该变量是一个常量,它的值在初始化后不能被修改。一旦给final变量赋值,就不能再更改它的值。例如:
    public class MyClass {
    final int myConstant = 42; // 常量,不能再修改值
    }

final的优势

  • 增加代码的可读性:final可以告诉其他开发人员,这个类、方法或变量是不可改变的,有助于理解代码的意图和设计意图。
  • 提高性能:final可以帮助编译器进行优化,并且可能有助于JIT(Just-In-Time)编译器生成更高效的代码。
  • 代码安全性:final可以避免类的继承和方法的重写,防止子类修改不应该被修改的行为。

Object根父类

在Java中,Object是所有类的根父类(Root Class),意味着每个Java类都直接或间接地继承自Object类。如果一个类没有明确指定继承自哪个类,那么它默认继承自Object类。
Object类是java.lang.Object包中的一个特殊类,它是Java语言中提供的最基本的类之一。Object类中定义了一些通用的方法,这些方法可以被所有其他类继承和使用。以下是Object类的一些重要方法:

方法 描述
equals(Object obj) 用于比较两个对象是否相等。默认情况下,equals方法比较对象的引用是否相同,即是否指向同一个内存地址。但在很多类中,会重写equals方法来比较对象的内容是否相等。
hashCode() 返回对象的哈希码值。在使用一些集合类如HashMap、HashSet等时,需要根据对象的hashCode值来判断对象是否相同。
toString() 返回对象的字符串表示。默认情况下,toString方法返回对象的类名和哈希码的十六进制表示。通常,我们会重写这个方法,以返回更有意义的对象描述。
getClass() 返回对象的运行时类(Runtime Class)对象。可以获取对象所属的类信息。
finalize() 在垃圾回收器回收对象之前调用该方法。但在现代Java中,不建议使用该方法来释放资源,应该使用try-finally块或AutoCloseable接口来更安全地释放资源。
notify(), notifyAll(), wait() 这些方法用于实现线程间的通信和同步。它们通常在多线程环境下使用。

静态

在Java中,静态(static)是一个关键字,它可以应用于类的成员变量、方法和代码块。静态成员属于类本身,而不是类的实例(对象),可以通过类名直接访问,无需创建类的实例。

静态变量

静态变量是属于类的属性,而不是对象的属性。它在类加载时创建,并且只有一个副本,被所有类的实例(对象)共享。静态变量通过static关键字声明,并且通常用于表示类级别的信息或配置项。静态变量在内存中始终存在,直到程序结束。

语法

[修饰符] class 类{
[其他修饰符] static 数据类型 静态变量名;
}

特点

  • 静态变量的默认值规则和实例变量一样。
  • 静态变量值是所有对象共享。
  • 静态变量的值存储在方法区。
  • 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。
  • 如果权限修饰符允许,在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。
  • 静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量”进行区分。

静态类变量和非静态实例变量、局部变量的区别

  • 静态类变量(简称静态变量):存储在方法区,有默认值,所有对象共享,生命周期和类相同,还可以有权限修饰符、final等其他修饰符
  • 非静态实例变量(简称实例变量):存储在堆中,有默认值,每一个对象独立,生命周期每一个对象也独立,还可以有权限修饰符、final等其他修饰符
  • 局部变量:存储在栈中,没有默认值,每一次方法调用都是独立的,有作用域,只能有final修饰,没有其他修饰符

静态方法

静态方法属于类而不属于对象,可以通过类名直接调用,无需创建类的实例。静态方法不能直接访问实例变量或实例方法,因为它们没有隐式的对象引用。

语法

[修饰符] class 类{
[其他修饰符] static 返回值类型 方法名(形参列表){
方法体
}
}

静态方法的特点

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
  • 只要权限修饰符允许,静态方法在其他类中可以通过“类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。
  • 静态方法可以被子类继承,但不能被子类重写。
  • 静态方法的调用都只看编译时类型。

示例代码:

public class MyMath {
public static int add(int a, int b) { // 静态方法,不依赖于实例
return a + b;
}
// 其他静态方法...
}

静态代码块

静态代码块用于在类加载时执行一些初始化操作。它只在类加载时执行一次,通常用于初始化静态变量或执行一些其他静态初始化工作。静态代码块在类加载时按照它们在类中的顺序执行。

语法

[修饰符] class 类{
static{
静态代码块
}
}

特点

  • 每一个类的静态代码块只会执行一次。
  • 静态代码块的执行优先于非静态代码块和构造器。

示例代码

public class MyClass {
static {
// 静态代码块,在类加载时执行一次
System.out.println("Static block executed.");
}
}

静态代码块和非静态代码块的区别

  • 静态代码块在类初始化时执行,只执行一次
  • 非静态代码块在实例初始化时执行,每次new对象都会执行

枚举

在Java中,枚举(Enumeration)是一种特殊的数据类型,用于表示一组常量。枚举常量在编译时被确定,并且在运行时不能被修改。Java中的枚举类型是一个类,通过关键字enum来定义。

枚举语法

枚举类型使用enum关键字来定义,枚举常量用逗号分隔。枚举常量默认是public、static和final的,它们的名称通常是大写字母。示例代码:

public enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}

枚举类的特点

  • 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
  • 如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略“;”。
  • 编译器给枚举类默认提供的是private的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数,
  • 如果枚举类需要的是有参构造,需要手动定义,有参构造的private可以省略,调用有参构造的方法就是在常量对象名后面加(实参列表)就可以。
  • 枚举类默认继承的是java.lang.Enum类,因此不能再继承其他的类型。
  • JDK1.5之后switch,提供支持枚举类型,case后面可以写枚举常量名。
  • 枚举类型如有其它属性,建议(不是必须)这些属性也声明为final的,因为常量对象在逻辑意义上应该不可变。

枚举常用方法

以下是Java枚举中常用的方法以及它们的描述:

方法 描述
String toString() 默认返回的是常量名(对象名),可以通过重写该方法来返回更有意义的描述。
String name() 返回的是常量名(对象名)。
int ordinal() 返回常量的次序号,默认从0开始。
枚举类型[] values() 返回该枚举类的所有常量对象,返回类型是当前枚举的数组类型,是一个静态方法。
枚举类型 valueOf(String name) 根据枚举常量对象名称获取枚举对象。

包装类

在Java中,包装类(Wrapper Class)是一组用于将基本数据类型转换为对象的类。Java的基本数据类型(如int、double、char等)是简单的数据类型,它们不是对象,不能参与面向对象编程的特性(如泛型、集合等)。为了让基本数据类型也能像对象一样使用,Java提供了对应的包装类。每个基本数据类型都有一个对应的包装类,这样可以在基本数据类型和对象之间进行转换。

包装类对应基础数据类型

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

使用包装类进行数据类型转换

  1. 将基本数据类型转换为对象:可以使用包装类将基本数据类型转换为对象,例如:
    int num = 42;
    Integer numObj = Integer.valueOf(num); // 将int转换为Integer对象
  2. 从对象获取基本数据类型的值:可以使用包装类从对象中获取基本数据类型的值,例如:
    Integer numObj = 42;
    int num = numObj.intValue(); // 从Integer对象获取int值
  3. 提供一些实用方法:包装类还提供了一些实用方法,例如将字符串转换为基本数据类型,比较两个对象的大小等。
    String numStr = "42";
    int num = Integer.parseInt(numStr); // 将字符串转换为int

抽象类

在Java中,抽象类(Abstract Class)是一种特殊的类,它不能被实例化,即不能创建对象。抽象类用于定义具有某些通用特征的类的结构,它可以包含抽象方法和具体方法。

抽象类语法

  1. 定义抽象类:
    使用abstract关键字来定义抽象类。抽象类可以包含抽象方法和非抽象方法。
    public abstract class Shape {
    // 抽象方法
    public abstract double calculateArea();

    // 具体方法
    public void display() {
    System.out.println("This is a shape.");
    }
    }
  2. 抽象方法:
    抽象方法是没有具体实现的方法,只有方法的声明,没有方法体。抽象方法用于定义接口,子类必须实现抽象方法。在抽象类中声明的抽象方法使用abstract关键字标记。
    public abstract double calculateArea();
  3. 非抽象方法:
    抽象类也可以包含非抽象方法,这些方法具有具体的实现。子类可以直接继承并使用非抽象方法。
    public void display() {
    System.out.println("This is a shape.");
    }
  4. 继承抽象类:
    可以通过继承抽象类来创建具体的子类。子类必须实现抽象类中的所有抽象方法,除非子类自己也声明为抽象类。
    public class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
    this.radius = radius;
    }

    @Override
    public double calculateArea() {
    return Math.PI * radius * radius;
    }
    }
  5. 抽象类的实例化:
    由于抽象类不能被实例化,所以不能直接创建抽象类的对象。但可以通过子类创建对象,利用多态性来引用抽象类的对象。
    Shape shape = new Circle(5.0);

接口

接口的作用就是一种统一的规范,让代码按照规范进行实现。这使得多个类可以具备相同的行为特性,即实现相同的接口,从而实现多继承的效果。同时,通过接口,我们可以实现多态性,即使用接口类型的引用来引用不同实现了接口的类的对象,从而统一对待不同对象,简化代码。

接口定义格式

[修饰符] interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法
// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}

示例代码:

public interface Usb3 {
//静态常量
long MAX_SPEED = 500 * 1024 * 1024;//500MB/s

//抽象方法
void in();

void out();

//默认方法
default void start() {
System.out.println("开始");
}

default void stop() {
System.out.println("结束");
}

//静态方法
static void show() {
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}

使用接口

接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。示例代码:

public class TestUsb3 {
public static void main(String[] args) {
//通过“接口名.”调用接口的静态方法
Usb3.show();
//通过“接口名.”直接使用接口的静态常量
System.out.println(Usb3.MAX_SPEED);
}
}

类实现接口

接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。 类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

语法

  1. 如果接口的实现类是非抽象类,那么必须==重写接口中所有抽象方法==。
  2. 默认方法可以选择保留,也可以重写。
  3. 接口中的静态方法不能被继承也不能被重写
【修饰符】 class 实现类  implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

使用接口的非静态方法

  • 对于接口的静态方法,直接使用“接口名.”进行调用即可;也只能使用“接口名.”进行调用,不能通过实现类的对象进行调用
  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用;接口不能直接创建对象,只能创建实现类的对象

代码示例:

public class TestMobileHDD {
public static void main(String[] args) {
//创建实现类对象
MobileHDD b = new MobileHDD();

//通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
b.start();
b.in();
b.stop();

//通过接口名调用接口的静态方法
// MobileHDD.show();
// b.show();
Usb3.show();
}
}

接口的多实现

在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
语法:

[修饰符] class 实现类  implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

[修饰符] class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

定义多个接口:接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。

public interface A {
void showA();
void show();
}
public interface B extends A {
void showB();
void show();
}

定义实现类:

public class C implements A,B {
@Override
public void showA() {
System.out.println("showA");
}

@Override
public void showB() {
System.out.println("showB");
}

@Override
public void show() {
System.out.println("show");
}
}

定义测试类:

public class TestC {
public static void main(String[] args) {
C c = new C();
c.showA();
c.showB();
c.show();
}
}

接口的多继承

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。
定义父接口:

public interface Chargeable {
void charge();
void in();
void out();
}

定义子接口:

public interface UsbC extends Chargeable,Usb3 {
void reverse();
}

自接口实现类:

public class TypeCConverter implements UsbC {
@Override
public void reverse() {
System.out.println("正反面都支持");
}

@Override
public void charge() {
System.out.println("可充电");
}

@Override
public void in() {
System.out.println("接收数据");
}

@Override
public void out() {
System.out.println("输出数据");
}
}

接口与实现类对象构成多态引用

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。
接口的不同实现类:

public class Mouse implements Usb3 {
@Override
public void out() {
System.out.println("发送脉冲信号");
}

@Override
public void in() {
System.out.println("不接收信号");
}
}
public class KeyBoard implements Usb3{
@Override
public void in() {
System.out.println("不接收信号");
}

@Override
public void out() {
System.out.println("发送按键信号");
}
}

测试类:

public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
Usb3 usb = new Mouse();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new KeyBoard();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new MobileHDD();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
}
}

接口特点总结

  • 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
  • 声明接口用interface,接口的成员声明有限制:(1)公共的静态常量(2)公共的抽象方法(3)公共的默认方法(4)公共的静态方法(5)私有方法(JDK1.9以上)
  • 类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。
  • 接口可以继承接口,关键字是extends,而且支持多继承。
  • 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

内部类

在Java中,内部类是一种定义在另一个类内部的类。它允许你在一个类中嵌套另一个类,并且可以访问外部类的成员。Java内部类提供了一种封装和组织代码的方式,同时还能够实现更复杂的设计模式和功能。

为什么需要内部类

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,不在其他地方单独使用,那么整个内部的完整结构最好使用内部类。而且内部类因为在外部类的里面,因此可以直接访问外部类的私有成员。

内部类的分类

  • 非静态内部类(成员内部类):这是最常见的内部类类型。它是定义在外部类的实例级别的,可以访问外部类的实例变量和方法。非静态内部类中不能包含静态成员(除了常量)。创建非静态内部类的实例需要先创建外部类的实例。
  • 静态内部类(静态嵌套类):这种内部类与外部类的实例无关,它可以像普通类一样使用,甚至可以创建没有外部类实例的情况下创建静态内部类的实例。静态内部类只能访问外部类的静态成员。
  • 方法内部类:这种内部类定义在方法内部,作用域被限制在该方法中。它可以访问外部类的所有成员,包括方法的参数和局部变量,但要求这些成员被声明为final。
  • 匿名内部类:匿名内部类没有显式的类名,通常用于创建只需要使用一次的简单类。它可以是抽象类或接口的实现,也可以是具体类的子类。

静态内部类

如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。语法格式:

[修饰符] class 外部类{
[其他修饰符] [static] class 内部类{
}
}

静态内部类的特点

  • 和其他类一样,它只是定义在外部类中的另一个完整的类结构:可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关;可以在静态内部类中声明属性、方法、构造器等结构,包括静态成员;可以使用abstract修饰,因此它也可以被其他类继承;可以使用final修饰,表示不能被继承;编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名和$符号。
  • 和外部类不同的是,它可以允许四种权限修饰符:public,protected,缺省,private;外部类只允许public或缺省的
  • 只可以在静态内部类中使用外部类的静态成员,在静态内部类中不能使用外部类的非静态成员。如果在内部类中有变量与外部类的静态成员变量同名,可以使用“外部类名.”进行区别。
  • 在外部类的外面不需要通过外部类的对象就可以创建静态内部类的对象

非静态内部类

没有static修饰的成员内部类叫做非静态内部类。

非静态内部类特点

  • 和其他类一样,它只是定义在外部类中的另一个完整的类结构:可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关;可以在非静态内部类中声明属性、方法、构造器等结构,但是不允许声明静态成员,但是可以继承父类的静态成员,而且可以声明静态常量;可以使用abstract修饰,因此它也可以被其他类继承;可以使用final修饰,表示不能被继承;编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名和$符号;和外部类不同的是,它可以允许四种权限修饰符:public,protected,缺省,private;外部类只允许public或缺省的
  • 还可以在非静态内部类中使用外部类的所有成员,哪怕是私有的
  • 在外部类的静态成员中不可以使用非静态内部类,就如同静态方法中不能访问本类的非静态成员变量和非静态方法一样
  • 在外部类的外面必须通过外部类的对象才能创建非静态内部类的对象;如果要在外部类的外面使用非静态内部类的对象,通常在外部类中提供一个方法来返回这个非静态内部类的对象比较合适;因此在非静态内部类的方法中有两个this对象,一个是外部类的this对象,一个是内部类的this对象

局部内部类

局部内部类是一种定义在方法内部的内部类。与其他类型的内部类不同,局部内部类的作用域仅限于定义它的方法内部,而且它只能在该方法内部被实例化和使用。局部内部类主要用于在方法内部封装一些复杂的逻辑或辅助功能,从而提高代码的可读性和封装性。

语法

[修饰符] class 外部类{
[修饰符] 返回值类型 方法名([形参列表]){
[final/abstract] class 内部类{
}
}
}

局部内部类的特点

  • 和外部类一样,它只是定义在外部类的某个方法中的另一个完整的类结构:可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关;可以在局部内部类中声明属性、方法、构造器等结构,但不包括静态成员,除非是从父类继承的或静态常量;可以使用abstract修饰,因此它也可以被同一个方法的在它后面的其他内部类继承;可以使用final修饰,表示不能被继承;编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
  • 和成员内部类不同的是,它前面不能有权限修饰符等
  • 局部内部类如同局部变量一样,有作用域
  • 局部内部类中是否能访问外部类的静态还是非静态的成员,取决于所在的方法
  • 局部内部类中还可以使用所在方法的局部常量,即用final声明的局部变量

匿名内部类

匿名内部类是一种没有显式类名的内部类,它是一种特殊的局部内部类。与其他内部类不同,匿名内部类没有类名,通常用于创建只需要使用一次的简单类,这样可以在不创建单独的类文件的情况下实现某些功能。匿名内部类通常用于实现接口的方法或扩展类的功能,且只在特定的代码块内部使用,使代码更加紧凑和简洁。

语法

new 父类(【实参列表】){
重写方法...
}
//()中是否需要【实参列表】,看你想要让这个匿名内部类调用父类的哪个构造器,如果调用父类的无参构造,那么()中就不用写参数,如果调用父类的有参构造,那么()中需要传入实参
new 父接口(){
重写方法...
}
//()中没有参数,因为此时匿名内部类的父类是Object类,它只有一个无参构造

匿名内部类的特点

  • 在匿名内部类中是否可以使用外部类的非静态成员变量,看所在方法是否静态
  • 在匿名内部类中如果需要访问当前方法的局部变量,该局部变量需要加final

匿名内部类的用途

使用匿名内部类的对象直接调用方法:

interface A{
void a();
}
public class Test{
public static void main(String[] args){
new A(){
@Override
public void a() {
System.out.println("aaaa");
}
}.a(); // 在创建匿名内部类的实例后,通过 .a() 调用了重写后的 a() 方法。
}
}

通过父类或父接口的变量多态引用匿名内部类的对象:

interface A{
void a();
}
public class Test{
public static void main(String[] args){
A obj = new A(){
@Override
public void a() {
System.out.println("aaaa");
}
};
obj.a();
}
}

匿名内部类的对象作为实参:

interface A{
void method();
}
public class Test{
public static void test(A a){
a.method();
}

public static void main(String[] args){
test(new A(){

@Override
public void method() {
System.out.println("aaaa");
}
});
}
}

注解

虽然说注解也是一种注释,因为它们都不会改变程序原有的逻辑,只是对程序增加了某些注释性信息。不过它又不同于单行注释和多行注释,对于单行注释和多行注释是给程序员看的,而注解是可以被编译器或其他程序读取的一种注释,程序还可以根据注解的不同,做出相应的处理。所以注解是插入到代码中以便有工具可以对它们进行处理的标签。

常用内置注解

注解 用途
@Override 用于标记方法是覆盖父类或接口的方法。
@Deprecated 用于标记已过时的方法或类,不建议继续使用。
@SuppressWarnings 用于抑制编译器对特定代码段产生的警告。
@FunctionalInterface 用于标记函数式接口,确保接口只有一个抽象方法,可以使用Lambda表达式。
@SafeVarargs 用于标记使用了可变参数的方法,并确保不会发生类型安全问题。

示例代码

package com.atguigu.annotation;

import java.util.ArrayList;

public class TestAnnotation {
@SuppressWarnings("all") // 告诉编译器忽略所有类型的警告
public static void main(String[] args) {
int i;

ArrayList list = new ArrayList();
list.add("hello");
list.add(123);
list.add("world");

Father f = new Son();
f.show();
f.methodOl();
}
}

class Father{
@Deprecated // 这表示该方法已过时,不建议继续使用。
void show() {
System.out.println("Father.show");
}
void methodOl() {
System.out.println("Father Method");
}
}

class Son extends Father{
/* @Override
void method01() {
System.out.println("Son Method");
}*/
}

异常

Java异常是在程序执行过程中出现的错误或异常情况的表示。当程序运行时发生异常,它会中断当前的正常执行流程,并跳转到异常处理机制来处理异常。Java中的异常是面向对象的,并通过异常类的层次结构来进行表示和处理。

异常机制

Java中把不同的异常用不同的类表示,一旦发生某种异常,就通过创建该异常类型的对象,并且抛出,然后程序员可以catch到这个异常对象,并处理,如果无法catch到这个异常对象,那么这个异常对象将会导致程序终止。
示例代码:

// src/main/java/org/example/ExceptionDemo.java
package org.example;

public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
int intnum = ArrayTools.getElement(arr, 4);
System.out.println("num=" + intnum);
System.out.println("over");
}
}
// src/main/java/org/example/ArrayTools.java
package org.example;

public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}

报错信息:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
at org.example.ArrayTools.getElement(ArrayTools.java:6)
at org.example.ExceptionDemo.main(ExceptionDemo.java:6)

异常的层次结构

  1. Throwable:是所有异常的根类,它分为两种类型:Error和Exception。
  2. Error:表示严重的错误,通常是无法恢复或不应该被捕获的错误,如OutOfMemoryError、StackOverflowError等。
  3. Exception:表示程序中的一般异常情况,它又分为两种类型:RuntimeException和非RuntimeException。
  4. RuntimeException:表示运行时异常,通常是由程序逻辑错误引起的,如NullPointerException、IndexOutOfBoundsException等。
  5. 非RuntimeException:表示非运行时异常,通常是由外部因素导致的,如IOException、SQLException等。

Throwable

throwable常用方法

方法 描述
getMessage() 返回异常的描述信息。
printStackTrace() 将异常及其跟踪信息输出到标准错误流,用于调试和异常诊断。
toString() 返回异常的字符串表示,通常包含异常类名和异常描述信息。
getCause() 返回引起当前异常的原因(cause)。

Error和Exception

Error

表示严重错误,一旦发生必须停下来查看问题并解决问题才能继续,无法仅仅通过try…catch解决的错误;例如:StackOverflowError(栈内存溢出)和OutOfMemoryError(堆内存溢出,简称OOM)。

Exception

表示普通异常,其它因编程错误或偶然的外在因素导致的一般性问题,程序员可以通过代码的方式检测、提示和纠正,使程序继续运行,但是只要发生也是必须处理,否则程序也会挂掉。

编译异常和运行异常

编译异常

在代码编译阶段,编译器就能明确警示当前代码可能发生(不是一定发生)xx异常,并督促程序员提前编写处理它的代码。如果程序员不听话,没有编写对应的异常处理代码,则编译器就会发威,直接判定编译失败,从而程序无法执行。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异常)。

运行异常

在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免的。例如:ArrayIndexOutOfBoundsException数组下标越界异常,ClassCastException类型转换异常。

异常处理

try…catch

语法

try{
可能发生xx异常的代码
}catch(异常类型1 e){
处理异常的代码1
}catch(异常类型2 e){
处理异常的代码2
}

示例代码

package org.example;

public class tryCatch {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
}
}

public static int divide(int num1, int num2) {
return num1 / num2;
}
}

finally

在Java中,finally 是一个关键字,它用于定义一个代码块,这个代码块中的代码无论发生什么情况,都会在 try-catch 结构中的代码执行后被执行。finally 块通常用于确保某些代码必定会被执行,无论是否发生异常。

语法

try {
// 可能会抛出异常的代码
} catch (ExceptionType1 e1) {
// 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e2) {
// 处理 ExceptionType2 类型的异常
} finally {
// 在异常处理后无论如何都会执行的代码
}

自定义异常

在Java中,自定义异常是指创建自己的异常类,以便在特定情况下抛出并捕获异常。通过自定义异常,可以更好地处理特定的业务逻辑异常,提供更有意义的异常信息,并将业务逻辑错误从系统错误中区分出来。自定义异常类应该继承自现有的异常类,通常继承自 Exception 或其子类。

自定义异常的优势

  • 提供更好的异常信息:可以在自定义异常类中定义适合业务场景的异常信息,便于开发者快速定位和解决问题。
  • 区分业务异常和系统异常:通过自定义异常,可以将业务逻辑错误与系统错误(例如空指针异常、IO异常等)区分开来,有助于更细致地进行异常处理。
  • 提高代码可读性:使用自定义异常可以使代码更具可读性和维护性,使得异常处理更加灵活和优雅。

    示例定义自定义异常

// src/main/java/org/example/MyCustomException.java
package org.example;

public class MyCustomException extends Exception {
public MyCustomException() {
super();
}

public MyCustomException(String message) {
super(message);
}

public MyCustomException(String message, Throwable cause) {
super(message, cause);
}

public MyCustomException(Throwable cause) {
super(cause);
}
}
// src/main/java/org/example/Example.java
package org.example;

public class Example {
public static void main(String[] args) {
try {
process();
} catch (MyCustomException e) {
System.out.println("Caught custom exception: " + e.getMessage());
}
}

public static void process() throws MyCustomException {
// Some business logic
// If an error occurs, throw the custom exception
throw new MyCustomException("Custom exception occurred!");
}
}

多线程

多线程是指在一个程序中同时执行多个线程,每个线程都可以独立地执行不同的任务。多线程技术允许程序在同一时间执行多个任务,从而提高程序的执行效率和性能。

多线程的优势

  1. 提高程序的执行效率:通过多线程并行执行任务,可以充分利用多核处理器的性能,加快程序的执行速度。
  2. 提高程序的响应性:在多线程程序中,一个线程可以继续执行其他任务,而不必等待当前任务完成,从而提高了程序的响应性和用户体验。
  3. 充分利用资源:多线程可以充分利用CPU和其他资源,提高系统的资源利用率。
  4. 简化程序设计:使用多线程可以将复杂的任务划分成多个简单的线程,简化程序的设计和实现。

线程与进程

进程

指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

进程与线程区别

进程:用来分配程序运行的资源,比如CPU、内存等。
线程:用来执行具体的任务。

并行与并发

并行

并行是指在同一时刻,多个任务同时进行执行,每个任务分配到不同的处理器核心或计算单元上,各任务彼此独立且同时进行。并行处理能够充分利用多核处理器的性能,通过将多个任务同时分配到不同的处理器核心上,实现任务的并行执行,从而加快整体计算速度。

并发

并发是指在同一时间段内,多个任务交替进行执行,虽然看起来是同时执行,但实际上任务之间可能是交替执行或并行执行的。并发处理使得系统能够同时处理多个任务,并实现资源的高效利用,但并不保证任务的真正同时执行。

并行与并发的却别

  • 并行是指多个任务在同一时刻同时进行执行,各任务互不干扰,每个任务独立执行在不同的处理器核心或计算单元上。
  • 并发是指多个任务在同一时间段内交替进行执行,任务之间可能交替执行或并行执行,但是可能在同一处理器核心上进行。
  • 并行的主要目标是加快计算速度,充分利用多核处理器的性能。
  • 并发的主要目标是提高系统的资源利用率,充分利用单核处理器的性能,并实现多任务交替执行。

创建进程

在Java中,可以使用ProcessBuilder类来创建和启动一个新的进程。ProcessBuilder类提供了一个简单的接口,可以让我们执行外部程序(如可执行文件)或系统命令。
示例代码:

import java.io.IOException;

public class CreateProcessExample {
public static void main(String[] args) {
try {
ProcessBuilder processBuilder = new ProcessBuilder("command", "argument1", "argument2");
Process process = processBuilder.start();

// 如果需要与新进程进行交互,可以使用 process.getInputStream() 和 process.getOutputStream()

int exitCode = process.waitFor();
System.out.println("Process exited with code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}

创建线程

在Java中,创建线程有两种常用的方式:一种是通过继承 Thread 类,另一种是通过实现 Runnable 接口。推荐使用实现 Runnable 接口的方式,因为Java支持单继承,如果已经继承了其他类,就无法再继承 Thread 类。
继承Thread类创建线程示例代码:

public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务逻辑
}
}

public class CreateThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程,调用 run() 方法
}
}

Runnable接口创建线程示例代码:

public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的任务逻辑
}
}

public class CreateThreadExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程,调用 run() 方法
}
}

Thread类

在Java中,Thread 类是用于创建和操作线程的核心类之一。它提供了一组方法来创建和管理线程,使得多线程编程变得更加简单和灵活。
通过继承 Thread 类:可以创建一个继承自 Thread 类的子类,并重写 run() 方法,在 run() 方法中定义线程的任务逻辑。然后通过创建子类的对象并调用 start() 方法来启动线程。
示例代码:

public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务逻辑
}
}
// 启动线程
MyThread thread = new MyThread();
thread.start();

Thread类内置方法

线程状态控制:

方法 描述
start() 启动线程,使其进入就绪状态,等待调度执行。
run() 线程执行的逻辑代码。需要在 start() 方法调用之前定义线程的任务逻辑,然后在 run() 方法中编写具体的执行代码。
sleep(long millis) 让线程休眠一段时间,暂时释放CPU,让其他线程有机会执行。
yield() 提示线程调度器可以切换到其他线程执行,但不保证一定切换。
join() 等待线程执行结束,阻塞当前线程直到目标线程执行完毕。
interrupt() 中断线程,给线程设置一个中断标志,由线程自行处理中断逻辑。

线程属性和信息获取:

方法 描述
getId() 获取线程的唯一标识符。
getName() 获取线程的名称。
getPriority() 获取线程的优先级。
isAlive() 判断线程是否还活着,即线程是否处于新建、就绪、运行或阻塞状态。
isDaemon() 判断线程是否是守护线程(Daemon Thread)。
setPriority(int newPriority) 设置线程的优先级。
setDaemon(boolean on) 将线程设置为守护线程。

Thread类构造方法

构造方法 描述
Thread() 创建一个新的线程对象。
Thread(Runnable target) 使用给定的 Runnable 对象创建一个新的线程对象。
Thread(Runnable target, String name) 使用给定的 Runnable 对象和线程名称创建一个新的线程对象。
Thread(String name) 创建一个新的线程对象,并指定线程名称。
Thread(Runnable target, String name, long stackSize) 使用给定的 Runnable 对象、线程名称和栈大小创建一个新的线程对象。

守护线程

守护线程(Daemon Thread)是一种特殊类型的线程,它是在程序中用于提供后台服务或支持性工作的线程。与普通线程(也称为用户线程)不同,守护线程并不会阻止程序的终止,即使所有的用户线程都已经结束,守护线程仍然会被自动终止。它们的生命周期与整个程序的生命周期相绑定。

示例代码

使用 Thread 类的构造方法可以创建守护线程。通过 setDaemon(true) 方法将线程设置为守护线程,该方法必须在调用 start() 方法之前设置。

public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(new MyRunnable());
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();

// 主线程执行一些任务
// ...
}
}

线程安全

线程安全是指在多线程环境下,共享的数据结构或对象能够被多个线程同时访问而不会出现问题,保证多线程同时进行操作时,不会导致数据不一致、数据丢失、死锁等问题。

同步机制

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称为它同步锁。

同步代码块和同步方法

同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。示例代码:

public synchronized void method(){
// 可能会产生线程安全问题的代码
}

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。示例代码:

synchronized(同步锁){
// 需要同步操作的代码
}

同步锁对象

同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
对于同步代码块来说,同步锁对象是由程序员手动指定的,但是对于同步方法来说,同步锁对象只能是默认的,

等待唤醒机制

进程间通讯

多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。而多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些通信机制,可以协调它们的工作,以此来帮我们达到多线程共同操作一份数据。

等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
就是在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定wait的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

  • wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING或TIMED_WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  • notify:则选取所通知对象的 wait set 中的一个线程释放;
  • notifyAll:则释放所通知对象的 wait set 上的全部线程。

文件处理

File类是java.io包下代表与平台无关的文件和目录,也就是说如果希望在程序中操作文件和目录都可以通过File类来完成,File类能新建、删除、重命名文件和目录。

构造方法

构造方法 描述
File(String pathname) 根据指定的路径名创建一个File实例。该路径名可以是绝对路径或相对路径。
File(String parent, String child) 根据指定的父路径和子路径创建一个File实例。
File(File parent, String child) 根据指定的父路径和子路径创建一个File实例。
File(URI uri) 根据指定的文件URI创建一个File实例。

示例代码:创建文件

package org.example;
import java.io.File;
import java.io.IOException;

public class MyDate{
public static void main(String[] args) throws IOException {
// 根据文件名创建文件
File file = new File("test.txt");
file.createNewFile();
// 通过父目录和文件名创建文件
File file1 = new File("src/main/java/org/example","test1.txt");
file1.createNewFile();
}
}

内置方法

方法 描述
exists() 判断文件或目录是否存在。
isFile() 判断当前路径表示的是否是一个文件。
isDirectory() 判断当前路径表示的是否是一个目录。
getName() 获取文件或目录的名称。
getPath() 获取文件或目录的路径。
getAbsolutePath() 获取文件或目录的绝对路径。
getParent() 获取文件或目录的父目录路径。
length() 获取文件的大小(字节数)。
canRead() 判断文件是否可读。
canWrite() 判断文件是否可写。
canExecute() 判断文件是否可执行(即是否是可运行程序)。
lastModified() 获取文件或目录的最后修改时间。
setLastModified(long time) 设置文件或目录的最后修改时间。
mkdir() 创建当前路径表示的目录。
mkdirs() 创建当前路径表示的目录,并创建其所有不存在的父目录。
createNewFile() 创建当前路径表示的文件。如果文件已存在,则不会创建,并返回 false
delete() 删除当前路径表示的文件或目录。如果是目录且不为空,则不会删除。
renameTo(File dest) 将当前路径表示的文件或目录重命名为指定的目标路径 dest
listFiles() 获取目录下的文件和子目录的 File 数组。
list() 获取目录下的文件和子目录的名称数组。

示例代码:判断test.txt是一个文件

package org.example;
import java.io.File;
import java.io.IOException;

public class MyDate{
public static void main(String[] args) throws IOException {
File file = new File("test.txt");
if (file.isFile()){
System.out.printf("%s is File",file.getPath());
}
}
}

IO概述

Java中I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。

IO的分类

  • 根据数据的流向可以分为:输入流(InputStream)和输出流(OutputStream);输入流:接受数据;输出流:发送数据。
  • 根据数据的类型可以分为:字节流和字符流;字节流:以字节为单位,读写数据的流;字符流:以字符为单位,读写数据的流。
  • 根据IO流的角色不同分为:节点流和处理流;节点流:可以从或向一个特定的地方(节点)读写数据;处理流:是对一个已存在的流进行连接和封装,通过所封装的流的功能调用实现数据读写。

常用的节点流

节点流类 用途
FileInputStream 用于从文件读取字节数据。
FileOutputStream 用于向文件写入字节数据。
FileReader 用于从文件读取字符数据。
FileWriter 用于向文件写入字符数据。
ByteArrayInputStream 用于从字节数组读取字节数据。
ByteArrayOutputStream 用于向字节数组写入字节数据。
StringReader 用于从字符串读取字符数据。
StringWriter 用于向字符串写入字符数据。
PipedInputStream 用于实现管道通信,用于线程之间的数据传输。
PipedOutputStream 用于实现管道通信,用于线程之间的数据传输。
ObjectInputStream 用于将对象序列化为字节流,并从中读取对象。
ObjectOutputStream 用于将对象序列化为字节流,并写入输出流。
DataInputStream 用于以基本数据类型为单位从输入流读取数据。
DataOutputStream 用于以基本数据类型为单位向输出流写入数据。

常见处理流

处理流类 用途
BufferedInputStream 增加缓冲区,提高字节输入流的读取效率。
BufferedOutputStream 增加缓冲区,提高字节输出流的写入效率。
BufferedReader 增加缓冲区,提高字符输入流的读取效率。
BufferedWriter 增加缓冲区,提高字符输出流的写入效率。
ObjectInputStream 用于从输入流中读取对象,并进行反序列化。
ObjectOutputStream 用于将对象序列化为字节流,并写入输出流。
DataInputStream 用于以基本数据类型为单位从输入流读取数据。
DataOutputStream 用于以基本数据类型为单位向输出流写入数据。
PrintStream 打印输出流,可以向输出流直接写入各种数据类型的值,并进行格式化。
PrintWriter 打印输出流,用于写入字符数据,并提供了方便的打印方法。
PushbackInputStream 可以将数据“推回”到输入流中,常用于处理特殊的标记和数据格式。
SequenceInputStream 可以将多个输入流串联在一起,形成一个逻辑上的大输入流。
LineNumberInputStream 继承自FilterInputStream,用于计算输入流的行号。
LineNumberReader 继承自BufferedReader,用于计算字符输入流的行号。
GZIPInputStream 用于解压缩使用GZIP压缩算法压缩的数据。
GZIPOutputStream 用于压缩输出数据,使用GZIP压缩算法。

字节流

一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

字节输出流

java.io.OutputStream `抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

FileOutputStream

OutputStream有很多子类,我们从最简单的一个子类开始。java.io.FileOutputStream `类是文件输出流,用于将数据写出到文件。
FileOutputStream内置方法:

方法 描述
void write(int b) 将指定的字节写入输出流。
void write(byte[] b) 将字节数组中的所有字节写入输出流。
void write(byte[] b, int off, int len) 将字节数组中从偏移量off开始的len个字节写入输出流。
void flush() 刷新输出流,将缓冲区的数据立即写入文件。
void close() 关闭输出流。如果流已经关闭,再调用该方法将没有任何作用。
FileChannel getChannel() 获取与该文件输出流关联的FileChannel对象,用于高级别的文件操作。
FileDescriptor getFD() 获取与该文件输出流关联的文件描述符。
void writeBoolean(boolean v) 将一个boolean值写入输出流,以单个字节形式。
void writeByte(int v) 将一个byte值写入输出流。
void writeShort(int v) 将一个short值写入输出流,以两个字节形式,高位在前。
void writeChar(int v) 将一个char值写入输出流,以两个字节形式。
void writeInt(int v) 将一个int值写入输出流,以四个字节形式,高位在前。
void writeLong(long v) 将一个long值写入输出流,以八个字节形式,高位在前。
void writeFloat(float v) 将一个float值写入输出流,以四个字节形式,按IEEE 754标准表示。
void writeDouble(double v) 将一个double值写入输出流,以八个字节形式,按IEEE 754标准表示。
void writeBytes(String s) 将字符串中的字符按字节形式写入输出流。
void writeChars(String s) 将字符串中的字符按字符形式写入输出流,每个字符占两个字节。
void writeUTF(String s) 将字符串以UTF-8编码形式写入输出流,使用可变长度形式进行编码。

当你创建一个流对象时,必须传入一个文件路径。如果该文件不存在,会创建该文件。如果有这个文件,会清空这个文件的数据。如果传入的是一个目录,则会报IOException异常。

示例代码:

package org.example;
import org.junit.Test;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileWrite {
@Test // @Test是JUnit测试框架中的一个注解,用于标识测试方法。当您在一个方法上添加@Test注解时,JUnit测试框架会将该方法标记为一个测试方法,并在执行测试时运行这些被标记的方法。
public void test1() throws IOException{
FileOutputStream fos = new FileOutputStream("fos.txt");
// 写入字节数据
fos.write(98);
fos.close();
}
@Test
public void test2() throws IOException{
FileOutputStream fos = new FileOutputStream("fos1.txt");
//写入字符串数据
byte[] b = "阿才的博客".getBytes();
fos.write(b);
fos.close();
}
}

数据追加写入

当您使用new FileOutputStream(“fos.txt”, true);时,其中的true参数表示以追加模式打开文件。在这种模式下,写入到FileOutputStream的任何数据都会被添加到文件的现有内容的末尾,而不会覆盖它。

参数值 打开模式 文件存在时的行为 文件不存在时的行为
false 写入模式 覆盖现有内容 创建新文件
true 追加模式 添加到文件末尾 创建新文件
package org.example;
import org.junit.Test;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileWrite {
@Test
public void test() throws IOException {
FileOutputStream fos = new FileOutputStream("fos1.txt", true);
byte[] b = "www.acaiblog.top".getBytes();
fos.write(b);
fos.close();
}
}

字节输入流

java.io.InputStream
java.io.InputStream抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
内置方法

方法签名 描述
int read() throws IOException 从输入流中读取下一个字节数据,并返回其值。如果已经到达流的末尾,则返回-1。
int read(byte[] b) throws IOException 从输入流中尝试读取最多b.length个字节数据,并将其存储在给定的字节数组b中。返回实际读取的字节数,如果已经到达流的末尾,则返回-1。
int read(byte[] b, int off, int len) throws IOException 从输入流中尝试读取最多len个字节数据,并将其存储在给定的字节数组b中,从数组的偏移量off位置开始存储。返回实际读取的字节数,如果已经到达流的末尾,则返回-1。
long skip(long n) throws IOException 跳过并丢弃输入流中的n个字节数据。返回实际跳过的字节数。
int available() throws IOException 返回可以从输入流中读取而不会被阻塞的估计剩余字节数。
void close() throws IOException 关闭输入流并释放与之相关的所有系统资源。

java.io.FileInputStream

java.io.FileInputStream `类是文件输入流,从文件中读取字节。

内置方法

方法签名 描述
int read() throws IOException 从输入流中读取下一个字节数据,并返回其值。如果已经到达流的末尾,则返回-1。
int read(byte[] b) throws IOException 从输入流中尝试读取最多b.length个字节数据,并将其存储在给定的字节数组b中。返回实际读取的字节数,如果已经到达流的末尾,则返回-1。
int read(byte[] b, int off, int len) throws IOException 从输入流中尝试读取最多len个字节数据,并将其存储在给定的字节数组b中,从数组的偏移量off位置开始存储。返回实际读取的字节数,如果已经到达流的末尾,则返回-1。
long skip(long n) throws IOException 跳过并丢弃输入流中的n个字节数据。返回实际跳过的字节数。
int available() throws IOException 返回可以从输入流中读取而不会被阻塞的估计剩余字节数。
void close() throws IOException 关闭输入流并释放与之相关的所有系统资源。
FileDescriptor getFD() throws IOException 返回与此文件输入流关联的文件描述符。
int readNBytes(byte[] b, int off, int len) throws IOException 从输入流中读取最多len个字节的数据,并将其存储在给定的字节数组b中,从数组的偏移量off位置开始存储。返回实际读取的字节数。
long skip(long n) throws IOException 跳过并丢弃输入流中的n个字节数据。返回实际跳过的字节数。

代码示例

当你创建一个流对象时,必须传入一个文件路径。如果文件不存在,会抛出FileNotFoundException 。如果传入的是一个目录,则会报IOException异常。
FileInputStream在读取文件时,实际上是按照字节的方式进行读取的。对于纯文本文件,如英文文本文件,每个字符通常占用一个字节,因此FileInputStream可以正确地读取和处理这些字符。
然而,中文字符通常使用多个字节表示,特别是在使用UTF-8编码的情况下。UTF-8编码中的中文字符通常由3个字节表示。FileInputStream只是单纯地读取字节,并不会对字节进行任何解码操作,因此如果直接使用FileInputStream读取包含中文字符的文件,会导致中文字符被拆分成多个字节,进而可能造成乱码或不正确的结果。

package org.example;
import org.junit.Test;
import java.io.IOException;
import java.io.FileInputStream;

public class FISRead {
@Test
public void test() throws IOException{
// fos1.txt内容abcde
FileInputStream fis = new FileInputStream("fos1.txt");
// read()方法每次读取一个字节,如果要读取多个字节需要调用多次read()方法
int read = fis.read();
System.out.println((char) read);
}
}

复制文件

package org.example;
import org.junit.Test;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class FISRead {
@Test
public void test() throws IOException{
FileInputStream fis = new FileInputStream("test.txt");
FileOutputStream fos = new FileOutputStream("test1.txt");
// 定义数组长度
byte[] b = new byte[1024];
int len;
// 如果fis.read(b) 不等于-1则一直循环读取,当fis.read(b)读取完返回-1则停止读取;fis.read([1024])表示每次读取的长度为1024个字节
while ((len = fis.read(b)) != -1){
fos.write(b,0,len);
}

}
}

字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

字符输入流

字符输入流Read

java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
内置方法:

方法签名 描述
int read() throws IOException 读取下一个字符,并返回其ASCII码值。如果已经到达流的末尾,则返回-1。
int read(char[] cbuf) throws IOException 尝试将最多cbuf.length个字符读入字符数组cbuf中。返回实际读取的字符数,如果已经到达流的末尾,则返回-1。
int read(char[] cbuf, int off, int len) throws IOException 尝试将最多len个字符读入字符数组cbuf中,从数组的偏移量off位置开始存储。返回实际读取的字符数,如果已经到达流的末尾,则返回-1。
long skip(long n) throws IOException 跳过并丢弃输入流中的n个字符。返回实际跳过的字符数。
boolean ready() throws IOException 检查此流是否已准备好被读取。
void close() throws IOException 关闭输入流并释放与之相关的所有系统资源。

字符输入流FileReader

java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
内置方法

方法签名 描述
int read() throws IOException 读取下一个字符,并返回其ASCII码值。如果已经到达流的末尾,则返回-1。
int read(char[] cbuf) throws IOException 尝试将最多cbuf.length个字符读入字符数组cbuf中。返回实际读取的字符数,如果已经到达流的末尾,则返回-1。
int read(char[] cbuf, int off, int len) throws IOException 尝试将最多len个字符读入字符数组cbuf中,从数组的偏移量off位置开始存储。返回实际读取的字符数,如果已经到达流的末尾,则返回-1。
long skip(long n) throws IOException 跳过并丢弃输入流中的n个字符。返回实际跳过的字符数。
boolean ready() throws IOException 检查此流是否已准备好被读取。
void close() throws IOException 关闭输入流并释放与之相关的所有系统资源。

示例代码

当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。如果该文件不存在,则报FileNotFoundException。如果传入的是一个目录,则会报IOException异常。

package org.example;
import org.junit.Test;

import java.io.FileReader;
import java.io.IOException;

public class fileReader {
@Test
public void test() throws IOException{
FileReader frd = new FileReader("read.txt");
int b;
// 将frd.read())的值赋值给b,如果b不为-1则继续允许读取数据
while ((b = frd.read()) != -1){
System.out.println((char) b);
}
frd.close();
}
}

字符输出流

Writer

java.io.Writer抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
内置方法

方法签名 描述
void write(int c) throws IOException 将指定的字符写入输出流。
void write(char[] cbuf) throws IOException 将字符数组cbuf的所有字符写入输出流。
void write(char[] cbuf, int off, int len) throws IOException 将字符数组cbuf中从偏移量off开始的len个字符写入输出流。
void write(String str) throws IOException 将字符串str写入输出流。
void write(String str, int off, int len) throws IOException 将字符串str中从偏移量off开始的len个字符写入输出流。
Writer append(CharSequence csq) throws IOException 将指定的字符序列csq追加到输出流。
Writer append(CharSequence csq, int start, int end) throws IOException 将指定字符序列csq中从startend范围的字符追加到输出流。
Writer append(char c) throws IOException 将指定的字符c追加到输出流。
void flush() throws IOException 刷新输出流,将缓冲区中的数据立即写入目标设备。
void close() throws IOException 关闭输出流并释放与之相关的所有系统资源。

FileWriter

java.io.FileWriter类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
内置方法

方法签名 描述
void write(int c) throws IOException 将指定的字符写入输出流。
void write(char[] cbuf) throws IOException 将字符数组cbuf的所有字符写入输出流。
void write(char[] cbuf, int off, int len) throws IOException 将字符数组cbuf中从偏移量off开始的len个字符写入输出流。
void write(String str) throws IOException 将字符串str写入输出流。
void write(String str, int off, int len) throws IOException 将字符串str中从偏移量off开始的len个字符写入输出流。
Writer append(CharSequence csq) throws IOException 将指定的字符序列csq追加到输出流。
Writer append(CharSequence csq, int start, int end) throws IOException 将指定字符序列csq中从startend范围的字符追加到输出流。
Writer append(char c) throws IOException 将指定的字符c追加到输出流。
void flush() throws IOException 刷新输出流,将缓冲区中的数据立即写入目标设备。
void close() throws IOException 关闭输出流并释放与之相关的所有系统资源。

代码示例

当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。如果文件不存在,则会自动创建。如果文件已经存在,则会清空文件内容,写入新的内容。

package org.example;
import org.junit.Test;
import java.io.FileWriter;
import java.io.IOException;

public class fileWriter {
@Test
public void test() throws IOException{
FileWriter fw = new FileWriter("test.txt");
fw.write("阿才的博客 www.acaiblog.top");
// FileWriter与FileOutputStream不同;如果不关闭,数据只是保存到缓冲区,并未保存到文件。
fw.close();
}
@Test
public void test1() throws IOException{
// true参数表示追加,默认false会清空后新增
FileWriter fw = new FileWriter("test1.txt",true);
fw.write("阿才的博客\n");
fw.write("https://www.acaiblog.top");
fw.close();
}
}

关闭和刷新

FileWriter与FileOutputStream不同。因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法了。

  • flush :刷新缓冲区,流对象可以继续使用。
  • close :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

示例代码:

package org.example;
import org.junit.Test;
import java.io.FileWriter;
import java.io.IOException;

public class fileWriter {
@Test
public void test() throws IOException{
FileWriter fw = new FileWriter("test.txt");
fw.write("aaa");
fw.flush();
fw.write("bbb");
fw.flush();
fw.close();
}
}

缓冲流

缓冲流(Buffered Stream)是Java I/O库提供的一组用于提高输入输出性能的类。它们在传统的输入输出流(非缓冲流)的基础上添加了内部缓冲区,通过预先读取或写入数据块来减少实际的I/O操作次数,从而提高读取和写入大量数据时的性能。
Java提供了两种类型的缓冲流:BufferedInputStream和BufferedOutputStream用于字节流,以及BufferedReader和BufferedWriter用于字符流。

缓冲流的优势

  • 减少I/O次数: 缓冲流通过在内部维护一个缓冲区来减少实际的I/O操作次数,从而提高读取和写入性能。
  • 提高效率: 当使用缓冲流读写大量数据时,相比于每次读取或写入一个字节或字符,缓冲流每次读取或写入一组数据,从而减少了系统调用的次数,提高了效率。
  • 支持标记和重置: 缓冲流支持mark()和reset()方法,可以在缓冲区内设置标记,然后在需要时回到该标记位置重新读取数据,提供了一些灵活性。
  • 自动刷新: 对于BufferedWriter,可以使用flush()方法强制将缓冲区内容刷新到目标设备,或者当BufferedWriter被关闭时,它会自动将缓冲区内容刷新到目标设备。

构造方法

BufferedInputStream:

构造方法 描述
BufferedInputStream(InputStream in) 创建一个新的缓冲输入流对象,它将数据从给定的输入流in读取,并添加了内部缓冲区。
BufferedInputStream(InputStream in, int size) 创建一个新的缓冲输入流对象,它将数据从给定的输入流in读取,并指定内部缓冲区的大小为size

BufferedOutputStream:

构造方法 描述
BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流对象,它将数据写入给定的输出流out,并添加了内部缓冲区。
BufferedOutputStream(OutputStream out, int size) 创建一个新的缓冲输出流对象,它将数据写入给定的输出流out,并指定内部缓冲区的大小为size

BufferedReader:

构造方法 描述
BufferedReader(Reader in) 创建一个新的缓冲输入流对象,它将字符数据从给定的字符输入流in读取,并添加了内部缓冲区。
BufferedReader(Reader in, int size) 创建一个新的缓冲输入流对象,它将字符数据从给定的字符输入流in读取,并指定内部缓冲区的大小为size

BufferedWriter:

构造方法 描述
BufferedWriter(Writer out) 创建一个新的缓冲输出流对象,它将字符数据写入给定的字符输出流out,并添加了内部缓冲区。
BufferedWriter(Writer out, int size) 创建一个新的缓冲输出流对象,它将字符数据写入给定的字符输出流out,并指定内部缓冲区的大小为size

代码示例:测试复制大文件使用缓冲流和不使用缓冲流所使用的时间

package org.example;
import org.junit.Test;

import java.io.*;

public class BufferIO {
public void test() throws IOException{
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("centos7.iso");
FileOutputStream fos = new FileOutputStream("centos7.iso.copy");
byte[] data = new byte[1024];
int len;
while ((len = fis.read(data)) != -1){
fos.write(data,0,len);
}
fos.close();
fis.close();
long end = System.currentTimeMillis();
System.out.printf("耗时: %d\n",(end-start));
}
public void test1() throws IOException{
long start = System.currentTimeMillis();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("centos7.iso"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("centos7.iso.copy"));
byte[] data = new byte[1024];
int len;
while ((len = bis.read(data)) != -1){
bos.write(data,0,len);
}
// 缓冲流的关闭顺序:先关外面的再关里面的
bos.close();
bis.close();
long end = System.currentTimeMillis();
System.out.printf("缓冲耗时: %d",(end-start));
}
public static void main(String[] args) throws IOException {
BufferIO obj = new BufferIO();
obj.test();
obj.test1();
}
}

结果:

耗时: 2920
缓冲耗时: 572

转换流

如果输出流和输入流编码不一致会导致数据乱码

java.io.InputStreamReader

方法签名 描述
InputStreamReader(InputStream in) 创建一个新的InputStreamReader对象,使用平台默认字符集来读取给定的字节输入流in
InputStreamReader(InputStream in, Charset cs) 创建一个新的InputStreamReader对象,使用指定的字符集cs来读取给定的字节输入流in
InputStreamReader(InputStream in, CharsetDecoder dec) 创建一个新的InputStreamReader对象,使用指定的字符集解码器dec来读取给定的字节输入流in
InputStreamReader(InputStream in, String charsetName) 创建一个新的InputStreamReader对象,使用指定的字符集名称charsetName来读取给定的字节输入流in
int read() throws IOException 读取一个字符并返回其Unicode代码点。如果已经到达流的末尾,则返回-1。
int read(char[] cbuf) throws IOException 尝试将最多cbuf.length个字符读入字符数组cbuf中。返回实际读取的字符数,如果已经到达流的末尾,则返回-1。
int read(char[] cbuf, int off, int len) throws IOException 尝试将最多len个字符读入字符数组cbuf中,从数组的偏移量off位置开始存储。返回实际读取的字符数,如果已经到达流的末尾,则返回-1。
boolean ready() throws IOException 检查此流是否已准备好被读取。
void close() throws IOException 关闭输入流并释放与之相关的所有系统资源。

java.io.OutputStreamWriter

方法签名 描述
OutputStreamWriter(OutputStream out) 创建一个新的OutputStreamWriter对象,使用平台默认字符集将字符写入给定的字节输出流out
OutputStreamWriter(OutputStream out, Charset cs) 创建一个新的OutputStreamWriter对象,使用指定的字符集cs将字符写入给定的字节输出流out
OutputStreamWriter(OutputStream out, CharsetEncoder enc) 创建一个新的OutputStreamWriter对象,使用指定的字符集编码器enc将字符写入给定的字节输出流out
OutputStreamWriter(OutputStream out, String charsetName) 创建一个新的OutputStreamWriter对象,使用指定的字符集名称charsetName将字符写入给定的字节输出流out
void write(int c) throws IOException 将指定字符写入输出流。
void write(char[] cbuf) throws IOException 将字符数组cbuf的所有字符写入输出流。
void write(char[] cbuf, int off, int len) throws IOException 将字符数组cbuf中从偏移量off开始的len个字符写入输出流。
void write(String str) throws IOException 将字符串str写入输出流。
void write(String str, int off, int len) throws IOException 将字符串str中从偏移量off开始的len个字符写入输出流。
void flush() throws IOException 刷新输出流,将缓冲区中的数据立即写入目标设备。
void close() throws IOException 关闭输出流并释放与之相关的所有系统资源。

示例代码

如果读取和写入编码不一致会乱吗

package org.example;
import org.junit.Test;

import java.io.*;

public class StreamRead {
@Test
public void test() throws IOException{
String fileName = "test.txt";
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fileName),"UTF-8");
osw.write("阿才的博客");
osw.close();
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8");
int len;
while ((len = isr.read()) != -1){
System.out.printf("%s", (char)len);
}

}
}

数据流与对象流

前面学习的IO流,在程序代码中,要么将数据直接按照字节处理,要么按照字符处理。那么,如果读写Java其他数据类型的数据需要使用数据流和对象流来处理这些类型的数据。

  • DataOutputStream:数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流(DataInputStream)将数据读入。
  • DataInputStream:数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。
  • ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。稍后可以使用 ObjectInputStream 将数据读入。通过在流中使用文件可以实现Java各种基本数据类型的数据以及对象的持久存储。如果流是网络套接字流,则可以在另一台主机上或另一个进程中接收这些数据或重构对象。
  • ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化。

对象流

对象流是Java中用于序列化和反序列化对象的流。它允许将Java对象转换为字节流,以便在网络上传输或保存到文件中,并且还可以从字节流中反序列化恢复为原始Java对象。Java提供了ObjectInputStream和ObjectOutputStream来支持对象的序列化和反序列化操作。

内置方法

ObjectOutputStream:

方法签名 描述
ObjectOutputStream(OutputStream out) 创建一个新的ObjectOutputStream对象,使用指定的字节输出流out
void writeObject(Object obj) throws IOException 将指定的对象obj写入输出流。注意,对象必须是可序列化的,否则会抛出java.io.NotSerializableException
void flush() throws IOException 刷新输出流,将缓冲区中的数据立即写入目标设备。
void close() throws IOException 关闭输出流并释放与之相关的所有系统资源。

ObjectInputStream:

方法签名 描述
ObjectInputStream(InputStream in) 创建一个新的ObjectInputStream对象,使用指定的字节输入流in
Object readObject() throws IOException 从输入流中读取一个对象,并将其反序列化为原始的Java对象。注意,对象必须是可序列化的,否则会抛出java.io.EOFExceptionjava.io.StreamCorruptedException
void close() throws IOException 关闭输入流并释放与之相关的所有系统资源。

对象输入流(java.io.DataOutputStream)支持java类型数据写入输出流中

方法签名 描述
public void writeBoolean(boolean val) 写入一个 boolean 值。
public void writeByte(int val) 写入一个8位字节。
public void writeShort(int val) 写入一个16位的 short 值。
public void writeChar(int val) 写入一个16位的 char 值。
public void writeInt(int val) 写入一个32位的 int 值。
public void writeLong(long val) 写入一个64位的 long 值。
public void writeFloat(float val) 写入一个32位的 float 值。
public void writeDouble(double val) 写入一个64位的 double 值。
public void writeUTF(String str) 写入UTF8数据。

示例代码

package org.example;
import org.junit.Test;

import java.io.*;

public class ObjectRead {
@Test
public void save() throws IOException{
String name = "阿才";
int age = 30;
boolean isBlog = true;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
oos.writeUTF(name);
oos.writeInt(age);
oos.writeBoolean(isBlog);
oos.close();
}
@Test
public void reload() throws IOException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
String name = ois.readUTF();
int age = ois.readInt();
boolean isBlog = ois.readBoolean();
System.out.printf("姓名: %s 年龄: %d 是否有博客: %b",name,age,isBlog);
}
}

序列化与反序列化

Java序列化和反序列化是一种将Java对象转换为字节流以便在网络上传输或保存到文件中,并且可以从字节流中还原回原始Java对象的过程。这样做可以在不同的Java进程之间传递和共享对象数据。

序列化

Java序列化是将Java对象转换为字节流的过程。要实现Java序列化,需要满足以下两个条件:

  • 必须实现 java.io.Serializable 接口,该接口是一个标记接口,不包含任何方法。通过实现这个接口,表明该类的对象是可序列化的。
  • 类的所有非静态成员变量都必须是可序列化的。如果类的成员变量中有某个对象不可序列化,那么这个类也将不能被序列化。

反序列化

Java反序列化是将字节流还原为原始Java对象的过程。要实现Java反序列化,需要满足以下条件:

  • 必须实现 java.io.Serializable 接口。
  • 反序列化的类和序列化的类必须具有相同的序列化ID(serialVersionUID)。可以通过显式声明一个固定的serialVersionUID来确保反序列化时的版本兼容性。

Serializable序列化接口与transient关键字

某个类的对象需要序列化输出时,该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

  • 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现Serializable 接口
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
  • 静态变量的值不会序列化。因为静态变量的值不属于某个对象。

示例代码

package org.example;
import java.io.Serializable;

public class Employee implements Serializable {
// static修饰的类变量,不会被序列化
public static String name;
public int age;
// transient修饰成员不会被序列化
public transient String address;
public Employee(String name, int age, String address){
this.name = name;
this.age = age;
this.address = address;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age = age;
}
@Override
public String toString(){
return "Employee{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
", age=" + age +
'}';
}
}
package org.example;
import org.junit.Test;
import org.example.Employee;

import java.io.*;

public class ReadWriteObject {
@Test
public void save() throws IOException{
Employee eml = new Employee("acai",30,"xian");
//创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("rto.txt"));
oos.writeObject(eml);
oos.close();
}
@Test
public void reload() throws IOException, ClassNotFoundException {
// 创建反序列化流
FileInputStream fis = new FileInputStream("rto.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Employee e = (Employee) ois.readObject();
ois.close();
fis.close();
System.out.printf(String.valueOf(e));
}
}

示例代码:创建多个序列化对象

package org.example;
import org.junit.Test;

import java.io.*;
import java.util.ArrayList;

public class ReadWriteMore {
@Test
public void save() throws IOException{
// 创建一个ArrayList以保存Employee对象
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee("张三",31,"广州"));
list.add(new Employee("王五",19,"深圳"));
//创建序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testmore.txt"));
// 写入对象
oos.writeObject(list);
oos.close();
}
@Test
public void reload() throws IOException, ClassNotFoundException {
// 创建一个FileInputStream以从文件中读取数据
FileInputStream fis = new FileInputStream("testmore.txt");
// 创建一个ObjectInputStream以从FileInputStream中读取对象
ObjectInputStream ois = new ObjectInputStream(fis);
// 从文件中读取ArrayList对象,并将其强制转换回ArrayList<Employee>
ArrayList<Employee> list = (ArrayList<Employee>) ois.readObject();
ois.close();
fis.close();
// 打印反序列化后的Employee对象的ArrayList
System.out.println(list);
}
}

网络编程

InetAddress

InetAddress是Java标准库中用于表示IP地址的类。它位于java.net包下,提供了一组静态方法和实例方法来处理IP地址和主机名。InetAddress类的主要作用是将主机名和IP地址之间进行转换,以及进行网络通信时的地址解析和操作。

内置方法

方法 描述
static InetAddress getLocalHost() 返回本地主机的InetAddress对象。
String getHostAddress() 获取InetAddress对象的IP地址。
String getHostName() 获取InetAddress对象的主机名。
static InetAddress getByName(String host) 根据主机名或IP地址字符串获取对应的InetAddress对象。
static InetAddress[] getAllByName(String host) 根据主机名获取所有对应的InetAddress对象。
String toString() 返回InetAddress对象的字符串表示。
boolean equals(Object obj) 判断两个InetAddress对象是否相等。
boolean isLoopbackAddress() 判断是否为环回地址(127.0.0.1)。
boolean isReachable(int timeout) 判断主机是否可以通过网络连接。

示例代码

package org.example;
import org.junit.Test;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class NetAddress {
@Test
public void test() throws UnknownHostException{
// 获取主机名和ip地址
InetAddress localhost = InetAddress.getLocalHost();
System.out.println(localhost);
}
@Test
public void test1() throws UnknownHostException{
// 解析域名的ip地址
InetAddress localhost = InetAddress.getByName("www.acaiblog.top");
System.out.println(localhost);
}
@Test
public void test2() throws UnknownHostException{
// 通过IP地址的字节数组来获取InetAddress对象
byte[] addr = {(byte) 192,(byte) 168,1,10};
InetAddress localhost = InetAddress.getByAddress(addr);
System.out.println(localhost);
}

}

socket

Java Socket 是 Java 编程语言提供的一种机制,用于在网络中实现通信。它允许在不同的设备之间建立连接,以便它们可以通过网络进行数据交换。Java Socket 提供了一种简单而强大的方式来实现客户端和服务器之间的通信,支持 TCP 和 UDP 协议。

socket分类

  • 流套接字(stream socket):使用TCP提供可依赖的字节流服务;ServerSocket:此类实现TCP服务器套接字。服务器套接字等待请求通过网络传入;Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
  • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务;DatagramSocket:此类表示用来发送和接收UDP数据报包的套接字。

socket类

描述
ServerSocket 用于创建服务器端套接字,用于侦听客户端连接请求。
Socket 用于创建客户端套接字,用于连接服务器并进行数据交换。

内置方法

ServerSocket 类方法:

方法 描述
ServerSocket(int port) 使用指定端口创建 ServerSocket。
Socket accept() 等待连接并返回一个新的 Socket 对象,用于与客户端进行通信。
void close() 关闭 ServerSocket。
int getLocalPort() 获取 ServerSocket 绑定的本地端口。
InetAddress getInetAddress() 获取 ServerSocket 绑定的本地地址。

Socket 类方法:

方法 描述
Socket(String host, int port) 创建一个 Socket 并连接到指定的服务器地址和端口。
InputStream getInputStream() 获取 Socket 的输入流,用于接收来自服务器的数据。
OutputStream getOutputStream() 获取 Socket 的输出流,用于向服务器发送数据。
void close() 关闭 Socket。
int getLocalPort() 获取 Socket 绑定的本地端口。
InetAddress getInetAddress() 获取与 Socket 关联的远程服务器地址。
int getPort() 获取与 Socket 关联的远程服务器端口。

TCP网络通讯

实现步骤

服务器:

  1. 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
  2. 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
  3. 调用 该Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和输入流,开始网络数据的发送和接收。
  4. 关闭Socket 对象:客户端访问结束,关闭通信套接字。

客户端:

  1. 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
  2. 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
  3. 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。
  4. 关闭 Socket :断开客户端到服务器的连接,释放线路

代码示例:实现单次通讯

package org.example;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import org.junit.Test;

public class socketServer {
@Test
public void server() throws IOException {
// 创建socket对象并绑定8888端口
ServerSocket server = new ServerSocket(8888);
// 等待客户端连接8888端口,该方法是一个阻塞方法如果客户端没有连接会一直等待
Socket socket = server.accept();
InetAddress addr = socket.getInetAddress();
System.out.printf("客户端连接地址: %s\n", addr);
// 获取输入流
InputStream input = socket.getInputStream();
byte[] data = new byte[1024];
StringBuilder s = new StringBuilder();
int len;
while ((len = input.read(data)) != -1){
s.append(new String(data,0,len));
}
System.out.printf("客户端发送的消息: %s", s);
// 获取输出流
OutputStream ops = socket.getOutputStream();
ops.write("欢迎登录".getBytes());
ops.flush();
socket.close();
server.close();

}
}
package org.example;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

import org.junit.Test;

public class socketClient {
@Test
public void client() throws IOException {
Socket socket = new Socket("127.0.0.1",8888);
// 获取输出流
OutputStream ops = socket.getOutputStream();
// 发送数据
ops.write("阿才的博客".getBytes());
socket.shutdownOutput();
// 获取输入流,获取服务器发送的消息
InputStream input = socket.getInputStream();
// 接受数据
byte[] data = new byte[1024];
// 创建StringBuilder空对象将数据添加到可变序列中
StringBuilder s = new StringBuilder();
int len;
while ((len = input.read(data)) != -1){
s.append(new String(data,0,len, StandardCharsets.UTF_8));
}
System.out.printf("服务器发送的消息: %s", s);

}
}

UDP网络通讯

实现步骤

发送端:

  • 创建DatagramSocket :默认使用系统随机分配端口号。
  • 创建DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长度,接收方的IP地址和端口号。
  • 调用该DatagramSocket 类对象的 send方法 :发送数据报DatagramPacket对象。
  • 关闭DatagramSocket 对象:发送端程序结束,关闭通信套接字。

接受端:

  • 创建DatagramSocket :指定监听的端口号。
  • 创建DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果,并指定最大可以接收的数据长度。
  • 调用 该DatagramSocket 类对象的receive方法 :接收数据报DatagramPacket对象。。
  • 关闭DatagramSocket :接收端程序结束,关闭通信套接字。

代码示例:udp发送接受数据

package org.example;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class udpReceive {
public static void main(String[] args) throws IOException {
// 建立udp连接
DatagramSocket ds = new DatagramSocket(9999);
byte[] data = new byte[1024*64];
DatagramPacket dp = new DatagramPacket(data,data.length);
while (true){
ds.receive(dp);
String str = new String(dp.getData(),0, dp.getLength());
System.out.println(str);
}

}
}
package org.example;

import java.io.IOException;
import java.net.*;
import java.util.ArrayList;

public class udpSent {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
ArrayList<String> all = new ArrayList<String>();
all.add("阿才的博客");
all.add("www.acaiblog.top");
// 接受端IP
InetAddress ip = InetAddress.getByName("127.0.0.1");
// 接受端端口
int port = 9999;
// 发送数据
for(int i=0;i<all.size();i++){
byte[] data = all.get(i).getBytes();
DatagramPacket dp = new DatagramPacket(data,0,data.length,ip,port);
// 调用socket发送方法
ds.send(dp);
}
}
}

反射

Java程序中,所有的对象都有两种类型:编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致。 例如:某些变量或形参的类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么如何解决呢? 为了解决这些问题,程序需要在运行时发现对象和类的真实信息,现在有两种方案:

  • 在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
  • 编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

什么是反射

加载完类之后,就产生了一个Class类型的对象,并将引用存储到方法区,那么每一个类在方法区内存都可以找到唯一Class对象与之对应,这个对象包含了完整的类的结构信息,我们可以通过这个对象获取类的结构。这种机制就像一面镜子,Class对象像是类在镜子中的镜像,通过观察这个镜像就可以知道类的结构,所以,把这种机制形象地称为反射机制。
Java的反射机制主要涉及以下几个核心类:

描述
Class类 用于表示类的类型,每个类在JVM中都有一个对应的Class对象,我们可以通过Class类来获取类的信息。
Field类 用于表示类的成员变量(字段)的信息。通过Field类,我们可以获取和修改类的字段值。
Method类 用于表示类的方法的信息。通过Method类,我们可以调用类的方法。
Constructor类 用于表示类的构造函数的信息。

java.lang.Class类

java.lang.Class是Java反射机制的核心类之一,它代表了Java中的类和接口类型。每个在JVM中运行的Java类都对应一个唯一的Class对象,它在运行时存储了该类的结构信息,包括类的字段、方法、构造函数、注解、父类、接口等信息。通过Class类,我们可以在运行时动态地获取类的信息并操作类的成员。

哪些类型可以获取Class对象?

类型 示例
基本数据类型 int.class void.class
类和接口 String.class Comparable.class
枚举 ElementType.class
注解 Override.class
数组 int[].class

示例代码:

package org.example;

import java.lang.annotation.ElementType;

public class javaType {
public static void main(String[] args){
//基本数据类型和void
Class c1 = int.class;
Class c2 = void.class;
System.out.printf("c1: %s\n", c1);
System.out.printf("c2: %s\n", c2);
// 类和接口
Class c3 = String.class;
Class c4 = Comparable.class;
System.out.printf("c3: %s\n", c3);
System.out.printf("c4: %s\n", c4);
// 枚举
Class c5 = ElementType.class;
System.out.printf("c5: %s\n", c5);
// 注解
Class c6 = Override.class;
System.out.printf("c6: %s\n", c6);
// 数组
Class c7 = String[].class;
Class c8 = int[].class;
System.out.printf("c7: %s\n",c7);
System.out.printf("c8: %s\n",c8);
}
}

运行结果:

c1: int
c2: void
c3: class java.lang.String
c4: interface java.lang.Comparable
c5: class java.lang.annotation.ElementType
c6: interface java.lang.Override
# [ 表示数组类型;Ljava.lang.String; 是 String 类的内部名称。
c7: class [Ljava.lang.String;
# [ 表示数组类型;I 是 int 类型的内部名称。与对象不同,Java 的基本类型在 JVM 内部有单个字符的内部名称表示。对于 int 类型,它的内部名称就是 I。
c8: class [I

类加载

类在内存中完整的生命周期:加载–>使用–>卸载

类加载的过程

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。

类加载的三个过程

  • 加载:load;就是指将类型的class字节码数据读入内存
  • 连接:link;验证:校验合法性;准备:准备对应的内存(方法区),创建Class对象,为类变量赋默认值,为静态常量赋初始值;解析:把字节码中的符号引用替换为对应的直接地址引用。
  • 初始化:initialize(类初始化)即执行类初始化方法,大多数情况下,类的加载就完成了类的初始化,有些情况下,会延迟类的初始化。

类的初始化

哪些操作会导致类初始化:

  1. 运行主方法所在的类,要先完成类初始化,再执行main方法
  2. 第一次使用某个类型就是在new它的对象,此时这个类没有初始化的话,先完成类初始化再做实例初始化
  3. 调用某个类的静态成员(类变量和类方法),此时这个类没有初始化的话,先完成类初始化
  4. 子类初始化时,发现它的父类还没有初始化的话,那么先初始化父类
  5. 通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化

类初始化执行的是(),该方法由类变量的显式赋值代码和静态代码块中的代码构成。示例代码:

package com.atguigu.init;

class Father{
static{
System.out.println("main方法所在的类的父类(1)");//初始化子类时,会初始化父类
}
}

public class ClassInitialize extends Father{
static{
System.out.println("main方法所在的类(2)");//主方法所在的类会初始化
}

public static void main(String[] args) throws ClassNotFoundException {
new A();//第一次使用A就是创建它的对象,会初始化A类

B.test();//直接使用B类的静态成员会初始化B类

Class clazz = Class.forName("com.atguigu.test02.C");//通过反射操作C类,会初始化C类
}
}
class A{
static{
System.out.println("A类初始化");
}
}
class B{
static{
System.out.println("B类初始化");
}
public static void test(){
System.out.println("B类的静态方法");
}
}
class C{
static{
System.out.println("C类初始化");
}
}

哪些操作不会导致类初始化:

  1. 使用某个类的静态的常量(static final)
  2. 通过子类调用父类的静态变量,静态方法,只会导致父类初始化,不会导致子类初始化,即只有声明静态成员的类才会初始化
  3. 用某个类型声明数组并创建数组对象时,不会导致这个类初始化

示例代码:

public class TestClinit2 {
public static void main(String[] args) {
System.out.println(D.NUM);//D类不会初始化,因为NUM是final的

System.out.println(F.num);
F.test();//F类不会初始化,E类会初始化,因为num和test()是在E类中声明的

//G类不会初始化,此时还没有正式用的G类
G[] arr = new G[5];//没有创建G的对象,创建的是准备用来装G对象的数组对象
//G[]是一种新的类型,是数组类想,动态编译生成的一种新的类型
//G[].class
}
}
class D{
public static final int NUM = 10;
static{
System.out.println("D类的初始化");
}
}
class E{
static int num = 10;
static{
System.out.println("E父类的初始化");
}
public static void test(){
System.out.println("父类的静态方法");
}
}
class F extends E{
static{
System.out.println("F子类的初始化");
}
}

class G{
static{
System.out.println("G类的初始化");
}
}

类加载器

很多开发人员都遇到过java.lang.ClassNotFoundException或java.lang.NoClassDefError,想要更好的解决这类问题,或者在一些特殊的应用场景,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作,那么需要你自定义类加载器,因此了解类加载器及其类加载机制也就成了每一个Java开发人员的必备技能之一。
类加载器的分类:

  1. 引导类加载器(Bootstrap Classloader)又称为根类加载器:它负责加载jre/rt.jar核心库;它本身不是Java代码实现的,也不是ClassLoader的子类,获取它的对象时往往返回null
  2. 扩展类加载器(Extension ClassLoader):它负责加载jre/lib/ext扩展库;它是ClassLoader的子类。
  3. 应用程序类加载器(Application Classloader):它负责加载项目的classpath路径下的类;它是ClassLoader的子类。
  4. 自定义类加载器:当你的程序需要加载“特定”目录下的类,可以自定义类加载器;当你的程序的字节码文件需要加密时,那么往往会提供一个自定义类加载器对其进行解码;

Java系统类加载器的双亲委托模式:
下一级的类加载器,如果接到任务时,会先搜索是否加载过,如果没有,会先把任务往上传,如果都没有加载过,一直到根加载器,如果根加载器在它负责的路径下没有找到,会往回传,如果一路回传到最后一级都没有找到,那么会报ClassNotFoundException或NoClassDefError,如果在某一级找到了,就直接返回Class对象。
应用程序类加载器把扩展类加载器视为父加载器, 扩展类加载器 把 引导类加载器视为父加载器。 不是继承关系,是组合的方式实现的。

查看某个类的类加载器对象

获取默认的系统类加载器:

ClassLoader ClassLoader.getSystemClassLoader()

查看某个类是哪个类加载器加载的:

#如果是根加载器加载的类,则会得到null
ClassLoader Class对象.getClassLoader()

获取某个类加载器的父加载器:

ClassLoader ClassLoader对象.getParent()

示例代码:

package org.example;

import org.junit.Test;

public class TestClassLoader {
@Test
public void test1(){
// 获取系统默认的加载类
ClassLoader SystemClassLoader = ClassLoader.getSystemClassLoader();
System.out.printf("获取系统默认的加载类: %s\n", SystemClassLoader);
}
@Test
public void test2() throws ClassNotFoundException {
// 获取String的加载类
ClassLoader c1 = String.class.getClassLoader();
System.out.printf("获取String的加载类: %s\n", c1);
// 加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器
ClassLoader c2 = Class.forName("sun.util.resources.cldr.zh.TimeZoneNames_zh").getClassLoader();
System.out.printf("加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器: %s\n", c2);
// 获取当前类的类加载器
ClassLoader c3 = TestClassLoader.class.getClassLoader();
System.out.printf("获取当前类的类加载器: %s\n", c3);
}
@Test
public void test3(){
// 获取当前类的类加载器
ClassLoader c1 = TestClassLoader.class.getClassLoader();
// 获取当前类父类加载器
ClassLoader c2 = c1.getParent();
System.out.printf("获取当前类父类加载器: %s", c2);
}
}

运行结果:

获取系统默认的加载类: sun.misc.Launcher$AppClassLoader@18b4aac2
获取String的加载类: null
加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器: sun.misc.Launcher$ExtClassLoader@45ff54e6
获取当前类的类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
获取当前类父类加载器: sun.misc.Launcher$ExtClassLoader@45ff54e6

反射的基本应用

获取类型的详细信息

可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上的)
示例代码获取常规信息:

package org.example;

import org.junit.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class TestClassInfo {
public Class test(){
// 先获取某个类型的class对象
Class classzz = String.class;
return classzz;
}
@Test
public void test1(){
// 调用test方法先获取某个类型的class对象
Class classzz = this.test();
// 获取包对象,所有java的包都是Package的对象
Package pkg = classzz.getPackage();
System.out.printf("获取String的包对象: %s\n", pkg);
// 获取修饰符
int mod = classzz.getModifiers();
System.out.printf("获取修饰符: %d\n", mod);
// 获取类型名
String name = classzz.getName();
System.out.printf("获取类型名: %s\n", name);
// 获取父类,父类也有对应的class对象
Class superclass = classzz.getSuperclass();
System.out.printf("获取父类: %s\n", superclass);
}
@Test
public void test2(){
// 调用test方法先获取某个类型的class对象
Class classzz = this.test();
// 获取父接口
Class[] interfaces = classzz.getInterfaces();
for(Class item : interfaces){
System.out.printf("获取父接口: %s\n", item);
}
/*
类的属性:声明一个属性,它是Field对象
Field clazz.getField(name) 根据属性名获取一个属性对象,但是只能得到公共的
Field[] clazz.getFields(); 获取所有公共的属性
Field clazz.getDeclaredField(name) 根据属性名获取一个属性对象,可以获取已声明的
Field[] clazz.getDeclaredFields() 获取所有已声明的属性
*/
// 获取类属性
Field[] declaredFields = classzz.getDeclaredFields();
for (Field item : declaredFields){
System.out.printf("获取类属性: %s\n",item);
}
}
@Test
public void test3(){
// 调用test方法先获取某个类型的class对象
Class classzz = this.test();
// 获取构造器
Constructor[] constructors = classzz.getDeclaredConstructors();
for (int i=0;i<constructors.length;i++){
Constructor constructor = constructors[i];
int modifiers = constructor.getModifiers();
String name = constructor.getName();
System.out.printf("第%d个构造器: 构造器修饰符: %s 构造器名称: %s\n",i,modifiers,name);
// 获取形参列表
Class[] parameterTypes = constructor.getParameterTypes();
for (Class parameterType:parameterTypes){
System.out.printf("获取形参列表: %s", parameterType);
}
// 异常列表
Class<?>[] exceptionTypes = constructor.getExceptionTypes();
for (Class<?> exceptionType : exceptionTypes){
System.out.printf("异常列表: %s", exceptionType);
}
}
}
@Test
public void test4(){
// 调用test方法先获取某个类型的class对象
Class classzz = this.test();
Method[] declaredMethods = classzz.getDeclaredMethods();
for (int i=0; i<declaredMethods.length; i++) {
Method method = declaredMethods[i];
System.out.println("第" + (i+1) +"个方法:");
//修饰符、返回值类型、方法名、形参列表 、异常列表
int modifiers = method.getModifiers();
System.out.println("方法的修饰符:" + Modifier.toString(modifiers));

Class<?> returnType = method.getReturnType();
System.out.println("返回值类型:" + returnType);

String name2 = method.getName();
System.out.println("方法名:" + name2);

//形参列表
System.out.println("形参列表:");
Class[] parameterTypes = method.getParameterTypes();
for (Class parameterType : parameterTypes) {
System.out.println(parameterType);
}

//异常列表
System.out.println("异常列表:");
Class<?>[] exceptionTypes = method.getExceptionTypes();
for (Class<?> exceptionType : exceptionTypes) {
System.out.println(exceptionType);
}
System.out.println();
}
}
}

创建任意引用类型的对象

两种方式:

  • 直接通过Class对象来实例化(要求必须有公共的无参构造)
  • 通过获取构造器对象来进行实例化

直接通过Class对象来实例化实现步骤:获取该类型的Class对象;创建对象
通过获取构造器对象来进行实例化实现步骤:获取该类型的Class对象;获取构造器对象;创建对象
示例代码:

package org.example;

import org.junit.Test;

public class TestCreateObject {
@Test
// 直接通过Class对象来实例化
public void test1() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class<?> clazz = Class.forName("org.example.Example");
Object obj = clazz.newInstance();
System.out.println(obj);
}
@Test
// 通过获取构造器对象来进行实例化
public void test2() throws ClassNotFoundException {
// 1. 获取Class对象
Class<?> clazz = Class.forName("org.example.Example");
// 2. 获取构造器对象
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
// 3. 创建实例对象
Object obj = constructor.newInstance("尚硅谷",2022);
System.out.println(obj);
}
}

操作任意类型的属性

获取该类型的Class对象

Class clazz = Class.forName("包.类名");

获取属性对象

Field field = clazz.getDeclaredField("属性名");

如果属性的权限修饰符不是public,那么需要设置属性可访问

field.setAccessible(true);

创建实例对象:如果操作的是非静态属性,需要创建实例对象

Object obj = clazz.newInstance(); //有公共的无参构造
Object obj = 构造器对象.newInstance(实参...);//通过特定构造器对象创建实例对象

设置属性值,如果操作静态变量,那么实例对象可以省略,用null表示

field.set(obj,"属性值");

获取属性值,如果操作静态变量,那么实例对象可以省略,用null表示

Object value = field.get(obj);

示例代码:

package org.example;

public class Student {
private int id;
private String name;
public int getId(){
return id;
}
public void setId(int id){
this.id = id;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
package org.example;

import java.lang.reflect.Field;

public class TestField {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException {
// 1.获取student对象
Class clazz = Class.forName("org.example.Student");
// 2.获取属性对象比如:id
Field idField = clazz.getDeclaredField("id");
// 3.如果id是私有属性设置id属性可访问
idField.setAccessible(true);
// 4.创建实例对象
Object obj = clazz.newInstance();
// 5.获取属性值
Object value = idField.get(obj);
System.out.printf("id: %d\n", value);
// 6.设置属性值
idField.set(obj,111);
value = idField.get(obj);
System.out.printf("id: %d", value);
}
}

调用任意类型的方法

获取该类型的Class对象

Class clazz = Class.forName("包.类名");

获取方法对象

Method method = clazz.getDeclaredMethod("方法名",方法的形参类型列表);

创建实例对象

Object obj = clazz.newInstance();

调用方法,如果方法的权限修饰符修饰的范围不可见,也可以调用setAccessible(true);如果方法是静态方法,实例对象也可以省略,用null代替。

Object result = method.invoke(obj, 方法的实参值列表);

示例代码:

package org.example;

import org.junit.Test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestMethod {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
// 1.获取student class对象
Class clazz = Class.forName("org.example.Student");
// 2.获取方法对象,在一个类中,唯一定位到一个方法,需要:(1)方法名(2)形参列表,因为方法可能重载;例如:void setName(String name)
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
// 3.创建对象实例
Object obj = clazz.newInstance();
// 4.调用方法
Object setNameMethodReturnValue = setNameMethod.invoke(obj,"阿才");
System.out.printf("obj: %s\n", obj);
//getName方法返回值类型String,有返回值,getNameMethod.invoke的返回值就是getName方法的返回值
System.out.printf("setNameMethodReturnValue: %s",setNameMethodReturnValue);
}
@Test
public void test1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> clazz = Class.forName("org.example.Student");
Method infoMethod = clazz.getMethod("getName",String.class);
System.out.printf("infoMethod: %s",infoMethod);
infoMethod.invoke(null,"阿才的博客");
System.out.printf("infoMethod: %s",infoMethod);
}
}

EMS项目

项目需求

模拟实现基于文本界面的员工管理系统,该软件能够实现员工对象的增删改查并能够打印员工明细表。项目采用分级菜单方式,主菜单功能如下:

  • 每个员工的信息被保存在Employee对象中。
  • 以一个Employee类型的数组来记录当前所有的员工。
  • 执行员工列表时,打印所有员工信息。

软件结构设计

该软件由以下四个模块组成:

  • EmployeeView: 负责菜单显示和处理用户操作
  • EmployeeService: 是Employee对象的管理模块,用数组管理Employee对象,并提供增删改查方法供EmployeeView调用
  • Employee: 为实体对象,用来封装员工信息
  • EmployeeMain: 程序入口

类设计

Employee:

字段 类型 说明
name String 姓名
gender char 性别
age int 年龄
salary double 工资
phone String 电话号码
email String 电子邮箱

EmployeeService:

方法名 用途 参数 返回值
EmployeeService(int totalCount) 构造器,用来初始化employees数组 totalCount: 指定employees数组的最大空间
addEmployee(Employee emp) 将参数emp对象添加组中最后一个员工对象记录之后 emp: 指定要添加的员工对象 添加成功返回true;false表示数组已满,无法添加
removeEmployee(int index) 从数组中删除参数index指定索引位置的员工对象记录 index: 指定所删除对象在数组中的索引位置 删除成功返回true;false表示索引无效,无法删除
getAllEmployees() 返回数组中记录的所有员工对象 Employee[] 数组中包含了当前所有员工对象,该数组长度与对象个数相同。
getEmployee(int index) 返回参数index指定索引位置的员工对象记录 index: 指定所要获取的员工对象在数组中的索引位置 封装了员工信息的Employee对象
  1. IDE创建EMS项目:
    ems.png
  2. 在com.atguigu.esm中创建main、service、view、domain这4个包,目录如下:
    .
    ├── pom.xml
    └── src
    └── main
    ├── java
    │   └── com
    │   └── atguigu
    │   └── ems
    │   ├── Main.java
    │   ├── domain
    │   ├── main
    │   ├── service
    │   └── view
    └── resources
  3. 在view包中定义EMSUtility.java类,用来实现键盘的访问
    package com.atguigu.ems.view;

    import java.util.Scanner;

    public class EMSUtility {
    private static Scanner scanner = new Scanner(System.in);

    public static char readMenuSelection() {
    char c;
    for (; ; ) {
    String str = readKeyBoard(1, false);
    c = str.charAt(0);
    if (c != '1' && c != '2' &&
    c != '3' && c != '4' && c != '5') {
    System.out.print("选择错误,请重新输入:");
    } else break;
    }
    return c;
    }

    public static char readChar() {
    String str = readKeyBoard(1, false);
    return str.charAt(0);
    }

    public static char readChar(char defaultValue) {
    String str = readKeyBoard(1, true);
    return (str.length() == 0) ? defaultValue : str.charAt(0);
    }

    public static int readInt() {
    int n;
    for (; ; ) {
    String str = readKeyBoard(11, false);
    try {
    n = Integer.parseInt(str);
    break;
    } catch (NumberFormatException e) {
    System.out.print("数字输入错误,请重新输入:");
    }
    }
    return n;
    }

    public static int readInt(int defaultValue) {
    int n;
    for (; ; ) {
    String str = readKeyBoard(11, true);
    if (str.equals("")) {
    return defaultValue;
    }

    try {
    n = Integer.parseInt(str);
    break;
    } catch (NumberFormatException e) {
    System.out.print("数字输入错误,请重新输入:");
    }
    }
    return n;
    }

    public static double readDouble() {
    double n;
    for (; ; ) {
    String str = readKeyBoard(12, false);
    try {
    n = Double.parseDouble(str);
    break;
    } catch (NumberFormatException e) {
    System.out.print("数字输入错误,请重新输入:");
    }
    }
    return n;
    }

    public static double readDouble(double defaultValue) {
    double n;
    for (; ; ) {
    String str = readKeyBoard(12, true);
    if (str.equals("")) {
    return defaultValue;
    }

    try {
    n = Double.parseDouble(str);
    break;
    } catch (NumberFormatException e) {
    System.out.print("数字输入错误,请重新输入:");
    }
    }
    return n;
    }

    public static String readString(int limit) {
    return readKeyBoard(limit, false);
    }

    public static String readString(int limit, String defaultValue) {
    String str = readKeyBoard(limit, true);
    return str.equals("")? defaultValue : str;
    }

    public static char readConfirmSelection() {
    char c;
    for (; ; ) {
    String str = readKeyBoard(1, false).toUpperCase();
    c = str.charAt(0);
    if (c == 'Y' || c == 'N') {
    break;
    } else {
    System.out.print("选择错误,请重新输入:");
    }
    }
    return c;
    }

    private static String readKeyBoard(int limit, boolean blankReturn) {
    String line = "";

    while (scanner.hasNextLine()) {
    line = scanner.nextLine();
    if (line.length() == 0) {
    if (blankReturn) return line;
    else continue;
    }

    if (line.length() < 1 || line.length() > limit) {
    System.out.print("输入长度(不大于" + limit + ")错误,请重新输入:");
    continue;
    }
    break;
    }

    return line;
    }
    }
  4. domain包中定义Employee类
    package com.atguigu.ems.domain;

    public class Employee {
    private String name; //员工姓名
    private char gender; //员工性别
    private int age; //员工年龄
    private double salary; //员工工资
    private String phone; //员工电话
    private String email; //员工邮箱
    public String getName(){return name;}
    public void setName(String name){this.name = name;}
    public char getGender(){return gender;}
    public void setGender(char gender){this.gender = gender;}
    public int getAge(){return age;}
    public void setAge(int age){this.age = age;}
    public double getSalary(){return salary;}
    public void setSalary(double salary){this.salary = salary;}
    public String getPhone(){return phone;}
    public void setPhone(String phone){this.phone = phone;}
    public String getEmail(){return email;}
    public void setEmail(String email){this.email = email;}
    public String say() {
    return name + "\t" + gender + "\t" + age + "\t" + salary + "\t" + phone + "\t" + email;
    }
    }
  5. service包中定义EmployeeService类
    package com.atguigu.ems.service;
    import com.atguigu.ems.domain.Employee;
    public class EmployeeService {
    private Employee[] employees; //声明一个Employee对象空数组,数组名称为employees
    private int realCount = 0; // 定义以保存员工数为0
    /**
    * 用途:构造器,用来初始化employees数组
    * 参数:totalCount:指定employees数组的最大空间
    */
    public EmployeeService(int totalCount){this.employees = new Employee[totalCount];}
    /**
    * 用途:将参数emp对象添加组中最后一个员工对象记录之后
    * 参数:emp指定要添加的员工对象
    * 返回:添加成功返回true;false表示数组已满,无法添加
    */
    public boolean addEmployee(Employee emp){
    if(realCount == this.employees.length){ //数组已满
    return false;
    }else {
    this.employees[realCount] = emp;
    realCount++;
    return true;
    }
    }
    /**
    * 用途:返回数组中记录的所有员工对象
    * 返回: Employee[] 数组中包含了当前所有员工对象,该数组长度与对象个数相同。
    * 返回的是完美数组, 数组是没有空洞的数组
    * @return
    */
    public Employee[] getAllEmployee(){
    Employee[] newArray = new Employee[realCount];
    for (int i=0;i<realCount;i++){
    newArray[i] = this.employees[i];
    }
    return newArray;
    }
    /**
    * 用途:返回参数index指定索引位置的员工对象记录
    * 参数: index指定所要获取的员工对象在数组中的索引位置
    * 返回:封装了员工信息的Employee对象
    */
    public Employee getEmployee(int index){
    if (index<0 || index>=realCount){
    return null;
    }
    return this.employees[index];
    }
    public boolean remoteEmployee(int index){
    if (index<0 || index>=realCount){
    return false;
    }
    // 1.把要删除的数组设置为null
    employees[index] = null;
    // 2.依次把右侧位置的值复制到左侧, 作用是补空洞
    for (int i=index;i<realCount-1;i++){
    employees[i] = employees[i+1];
    }
    // 3.把之前的最后有效位置置空
    employees[realCount-1] = null;
    // 4.调整计数器
    realCount--;
    return true;
    }

    }
  6. view包中创建EmployeeView类
    package com.atguigu.ems.view;
    import com.atguigu.ems.service.EmployeeService;
    import com.atguigu.ems.domain.Employee;
    public class EmployeeView {
    // 关联到的管理器对象, 内部的数组长度在这里写死成10个, 允许最多管理10个员工
    private EmployeeService empService = new EmployeeService(10);
    private void listAllEmployees() {
    System.out.println("---------------------------------员工列表--------------------------------------");
    System.out.println("编号\t姓名\t性别\t年龄\t工资\t\t电话\t\t邮箱");
    // 真的获取所有员工
    Employee[] allEmployees = empService.getAllEmployee();
    for (int i = 0; i < allEmployees.length; i++) {
    System.out.println((i + 1) + "\t" + allEmployees[i].say());
    }
    System.out.println("-----------------------------员工列表完成-------------------------------------");
    }
    private void addNewEmployee(){
    System.out.println("---------------------添加员工---------------------");
    System.out.print("姓名:");
    String name = EMSUtility.readString(10);
    System.out.print("性别:");
    char gender = EMSUtility.readChar();
    System.out.print("年龄:");
    int age = EMSUtility.readInt();
    System.out.print("工资:");
    double salary = EMSUtility.readDouble();
    System.out.print("电话:");
    String phone = EMSUtility.readString(20);
    System.out.print("邮箱:");
    String email = EMSUtility.readString(30);
    // 创建Employee对象
    Employee employee = new Employee();
    employee.setName(name);
    employee.setGender(gender);
    employee.setAge(age);
    employee.setSalary(salary);
    employee.setPhone(phone);
    employee.setEmail(email);

    // 通过调用管理器对象完成 员工添加
    boolean flag = empService.addEmployee(employee);
    if (flag) {
    System.out.println("---------------------添加完成---------------------");
    listAllEmployees();
    } else {
    System.out.println("---------------------添加失败---------------------");
    }
    }
    private void modifyEmployee () {
    System.out.println("---------------------修改员工---------------------");
    listAllEmployees();
    System.out.print("请选择待修改员工编号(-1退出):");
    // 获取用户输入的编号
    int no = EMSUtility.readInt();
    if (no == -1) {
    return;
    }
    // 根据编号定位要修改的目标对象
    Employee target = empService.getEmployee(no - 1);
    if (target == null) {
    System.out.println("--------------指定编号[" + no + "]的员工不存在-----------------");
    return;
    }
    System.out.println("<直接回车表示不修改>");
    System.out.print("姓名(" + target.getName() + "):");
    String name = EMSUtility.readString(10, target.getName());
    target.setName(name);
    System.out.print("性别(" + target.getGender() + "):");
    char gender = EMSUtility.readChar(target.getGender());
    target.setGender(gender);
    System.out.print("年龄(" + target.getAge() + "):");
    int age = EMSUtility.readInt(target.getAge());
    target.setAge(age);
    System.out.print("工资(" + target.getSalary() + "):");
    double salary = EMSUtility.readDouble(target.getSalary());
    target.setSalary(salary);
    System.out.print("电话(" + target.getPhone() + "):");
    String phone = EMSUtility.readString(20, target.getPhone());
    target.setPhone(phone);
    System.out.print("邮箱(" + target.getEmail() + "):");
    String email = EMSUtility.readString(50, target.getEmail());
    target.setEmail(email);
    System.out.println("---------------------修改完成---------------------");
    listAllEmployees();
    }
    private void deleteEmployee () {
    System.out.println("---------------------删除员工---------------------");
    listAllEmployees();
    System.out.print("请选择待删除员工编号(-1退出):");
    // 获取用户输入的编号
    int no = EMSUtility.readInt();
    if (no == -1) {
    return;
    }
    System.out.print("确认是否删除(Y/N):");
    // 获取用户输入的确认
    char confirm = EMSUtility.readConfirmSelection();
    if (confirm == 'Y') {
    boolean flag = empService.remoteEmployee(no - 1);
    if (flag) {
    System.out.println("---------------------删除完成---------------------");
    listAllEmployees();
    } else {
    System.out.println("---------------------删除失败---------------------");
    }
    }
    }
    public void enterMainMenu(){
    boolean loopFlag = true;
    do{
    System.out.println("##########EMS员工管理系统##########");
    System.out.println(" 1.添加员工");
    System.out.println(" 2.修改员工");
    System.out.println(" 3.删除员工");
    System.out.println(" 4.员工列表");
    System.out.println(" 5.退 出");
    System.out.println();
    System.out.print("请选择:");
    // 读取用户选择
    char choice = EMSUtility.readMenuSelection();
    switch (choice) {
    case '1' : addNewEmployee(); break;
    case '2' : modifyEmployee(); break;
    case '3' : deleteEmployee(); break;
    case '4' : listAllEmployees(); break;
    case '5' :
    System.out.print("确认是否退出(Y/N):");
    // 获取用户输入的确认
    char confirm = EMSUtility.readConfirmSelection();
    if (confirm == 'Y') {
    loopFlag = false;
    }
    break;
    }
    } while (loopFlag);
    }
    }
  7. main包中创建EmployeeMain类
    package com.atguigu.ems.main;

    import com.atguigu.ems.view.EmployeeView;

    public class EmployeeMain {
    public static void main(String[] args) {
    EmployeeView employeeView = new EmployeeView();
    employeeView.enterMainMenu();
    }
    }

JDBC

JDBC:Java Database Connectivity,它是代表一组独立于任何数据库管理系统(DBMS)的API,声明在java.sql与javax.sql包中,是SUN(现在Oracle)提供的一组接口规范。由各个数据库厂商来提供实现类,这些实现类的集合构成了数据库驱动jar。即JDBC技术包含两个部分:

  • java.sql包和javax.sql包中的API
  • 各个数据库厂商提供的jar

Java连接MySQL数据库

  1. 在官网下载mariadb驱动,下载地址:https://downloads.mariadb.com/Connectors/java/connector-java-3.1.4/mariadb-java-client-3.1.4.jar
  2. 将下载的mariadb-java-client-3.1.4.jar放到项目lib目录中
  3. 点击mariadb-java-client-3.1.4.jar单击右键Add as Library

示例代码:

package org.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class TestJDBC {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Class.forName("org.mariadb.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mariadb://acaiblog.top:3306/test?serverTimezone=UTC","root","123456");
System.out.printf("conn: %s", conn);
conn.close();
}
}

连接mysql常见错误

  1. 错误日志:
    The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
    at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
    at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825)
    at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:446)
    at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:239)
    at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:188)
    at java.sql.DriverManager.getConnection(DriverManager.java:664)
    at java.sql.DriverManager.getConnection(DriverManager.java:247)
    at org.example.TestJDBC.main(TestJDBC.java:10)
    Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
    错误原因:网络或端口不通
  2. 错误日志:
    Exception in thread "main" java.sql.SQLException: Unknown system variable 'transaction_isolation' 
    错误原因:驱动版本与数据库版本不匹配

    增删改查

    数据库表设计

创建表sql:

CREATE TABLE t_department (
did INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
dname VARCHAR(20) NOT NULL UNIQUE,
description VARCHAR(200)
);

增加数据

package org.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class DBInsert {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 驱动加载到内存中
Class.forName("org.mariadb.jdbc.Driver");
// 获取数据库连接对象
String url = "jdbc:mariadb://acaiblog.top:3306/test?serverTimezone=UTC";
// Connection相当于网络编程的socket
Connection conn = DriverManager.getConnection(url,"root","123456");
String sql = "insert into t_department values(null,'测试数据部门','测试数据部门简介')";
// 发送sql到数据库执行
PreparedStatement pst = conn.prepareStatement(sql);
int len = pst.executeUpdate();
System.out.println(len>0 ? "添加成功" : "添加失败");
pst.close();
conn.close();
}
}

修改数据

package org.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class DBInsert {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 驱动加载到内存中
Class.forName("org.mariadb.jdbc.Driver");
// 获取数据库连接对象
String url = "jdbc:mariadb://acaiblog.top:3306/test?serverTimezone=UTC";
// Connection相当于网络编程的socket
Connection conn = DriverManager.getConnection(url,"root","123456");
// String sql = "insert into t_department values(null,'测试数据部门','测试数据部门简介')";
String sql = "update t_department set description = 'xx' where did = 1";
// 发送sql到数据库执行
PreparedStatement pst = conn.prepareStatement(sql);
int len = pst.executeUpdate();
System.out.println(len>0 ? "修改成功" : "修改失败");
pst.close();
conn.close();
}
}

查询数据

package org.example;

import java.sql.*;

public class DBInsert {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 驱动加载到内存中
Class.forName("org.mariadb.jdbc.Driver");
// 获取数据库连接对象
String url = "jdbc:mariadb://acaiblog.top:3306/test?serverTimezone=UTC";
// Connection相当于网络编程的socket
Connection conn = DriverManager.getConnection(url,"root","123456");
// String sql = "insert into t_department values(null,'测试数据部门','测试数据部门简介')";
String sql = "select * from t_department";
// 发送sql到数据库执行
PreparedStatement pst = conn.prepareStatement(sql);
ResultSet resultSet = pst.executeQuery();
while (resultSet.next()){
int did = resultSet.getInt("did");//get一次得到一个单元格的数据
String dname = resultSet.getString("dname");
String decription = resultSet.getString("description");
System.out.println(did +"\t" + dname +"\t" + decription);
}
resultSet.close();
pst.close();
conn.close();
}
}

删除数据

package org.example;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class DBInsert {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 驱动加载到内存中
Class.forName("org.mariadb.jdbc.Driver");
// 获取数据库连接对象
String url = "jdbc:mariadb://acaiblog.top:3306/test?serverTimezone=UTC";
// Connection相当于网络编程的socket
Connection conn = DriverManager.getConnection(url,"root","123456");
String sql = "delete from t_department where did = 1";
// 发送sql到数据库执行
PreparedStatement pst = conn.prepareStatement(sql);
int len = pst.executeUpdate();
System.out.println(len>0 ? "删除成功" : "删除失败");
pst.close();
conn.close();
}
}

数据库连接池

什么是数据库连接池

数据库连接池是一种用于优化数据库连接管理的技术。在应用程序与数据库之间建立连接是一项开销较大的操作,因为涉及到网络通信、认证和初始化数据库连接等步骤。为了避免频繁地打开和关闭数据库连接,以及减轻数据库的负担,引入数据库连接池是一个常见的解决方案。
数据库连接池是一个维护着预先创建的、可重复使用的数据库连接的缓冲区。当应用程序需要与数据库交互时,它从连接池中获取一个空闲的数据库连接,而不是每次都创建一个新的连接。这样可以避免频繁地创建和销毁连接,提高了应用程序的性能和响应速度。

数据库连接池的特点

  1. 连接复用:连接池会在应用程序启动时创建一定数量的数据库连接,并将它们保持在活动状态。当应用程序需要连接数据库时,它会从连接池中获取一个已经存在且空闲的连接,而不是重新创建一个。
  2. 连接管理:连接池会负责管理连接的状态。在使用完连接后,连接池可以将连接标记为空闲状态,以便其他请求可以继续重复使用这个连接。
  3. 连接数量控制:连接池可以限制可以创建的连接数量,以防止连接过多导致数据库性能下降。
  4. 连接超时处理:连接池可以实现连接的超时处理机制,避免由于数据库故障或网络问题导致连接长时间占用而不释放。

常见的数据库连接池

连接池名称 描述
Apache Commons DBCP Apache 软件基金会提供的开源数据库连接池库。
C3P0 一个稳定且功能丰富的开源 JDBC 数据库连接池。
HikariCP 高性能的开源 JDBC 数据库连接池,性能优秀。
BoneCP 另一个高性能的开源 JDBC 连接池,低延迟和高吞吐量。
Tomcat JDBC Connection Pool Apache Tomcat 服务器内置的连接池,也可以单独使用。
Druid 阿里巴巴开发的强大开源 JDBC 连接池,具备监控和防火墙特性。

使用Druid数据库连接池的步骤

  1. 下载驱动,引入jar包,和引入mysql驱动jar方式一样;下载地址:http://www.java2s.com/ref/jar/druid-1.1.9.jar.zip
  2. 编写配置文件,将druid.properties文件移动到resources目录下
  3. 创建数据库连接池对象
  4. 获取连接
#druid.properties
driverClassName=org.mariadb.jdbc.Driver
url=jdbc:mariadb://acaiblog.top:3306/test?serverTimezone=UTC&rewriteBatchedStatements=true
username=root
password=123456
initialSize=5
maxActive=10
maxWait=1000
validationQuery=SELECT 1
package org.example;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import com.alibaba.druid.pool.DruidDataSourceFactory;

public class TestPool {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
//因为druid.properties文件是在src下,最后会随着.java文件一起编译到类路径下(class)可以通过类加载器帮我们加载资源配置文件
properties.load(TestPool.class.getClassLoader().getResourceAsStream("druid.properties"));
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 演示获取15个连接对象
for (int i=1;i<=15;i++){
new Thread(){
public void run() {
Connection connection = null;
try {
connection = dataSource.getConnection();
System.out.printf("connection: %s\n", connection);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}.start();
}
}
}

封装DAO层代码

什么是DAO

DAO是数据访问对象(Data Access Object)的缩写,是一种设计模式,在软件开发中用于将业务逻辑和数据访问逻辑分离。它的主要目的是为了提高代码的可维护性、可扩展性和代码复用性。
DAO模式的主要思想是将数据访问逻辑封装在独立的数据访问对象中,这样其他部分的代码(比如服务层或业务逻辑层)就不需要直接与数据存储(数据库、文件系统等)打交道。通过DAO接口和实现类,业务逻辑层可以通过调用DAO提供的方法来访问数据,而不用关心数据的存储细节。
通常,一个DAO接口定义了一组对数据进行增、删、改、查等操作的方法,而DAO的具体实现类则负责实现这些方法并和底层的数据存储进行交互。这样的设计使得更换数据存储系统或者数据访问层的实现变得相对容易,而不会对上层业务逻辑产生影响。

DAO的优势

  • 代码的解耦和模块化:业务逻辑和数据访问逻辑分离,使代码更易于维护和理解。
  • 更好的可扩展性:如果需要切换到其他数据存储系统,只需实现新的DAO实现类,不用修改其他代码。
  • 代码复用性:DAO提供了一组通用的数据访问接口,可以被不同的业务逻辑模块复用。

示例代码

  1. 使用以下sql创建表
    CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL
    );
  2. 在pom.xml中添加以下依赖
    <dependency>
    <groupId>org.mariadb.jdbc</groupId>
    <artifactId>mariadb-java-client</artifactId>
    <version>3.0.7</version>
    </dependency>
  3. 定义用户实体类User.java
    package org.example;

    public class User {
    private int id;
    private String username;
    private String email;

    public User(int id, String username, String email) {
    this.setUsername(username);
    this.setEmail(email);
    this.setId(id);
    }

    public int getId(){return id;}
    public void setId(int id){this.id = id;}
    public String getUsername(){return username;}
    public void setUsername(String username){this.username = username;}
    public String getEmail(){return email;}
    public void setEmail(String email){this.email = email;}

    public String toString(){
    return "User [id=" + id + ", username=" + username + ", email=" + email + "]";
    }

    }
  4. 定义数据访问对象的接口 UserDao.java
    package org.example;
    import java.sql.SQLException;
    import java.util.List;

    public interface UserDao {
    User findById(int id) throws SQLException;
    List<User> findAll();
    void save(User user);
    void update(User user);
    void delete(int id);
    }
  5. 实现数据访问对象的接口 UserDaoImpl.java
    package org.example;

    import java.sql.*;
    import java.util.ArrayList;
    import java.util.List;

    public class UserDaoImpl implements UserDao{
    private static final String DB_URL = "jdbc:mariadb://acaiblog.top:3306/test";
    private static final String DB_USERNAME = "root";
    private static final String DB_PASSWORD = "123456";
    @Override
    public User findById(int id) {
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    try {
    connection = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);
    preparedStatement = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
    preparedStatement.setInt(1,id);
    resultSet = preparedStatement.executeQuery();
    if (resultSet.next()){
    return maptoUser(resultSet);
    }
    } catch (SQLException e) {
    e.printStackTrace();
    } finally {
    if (resultSet != null){
    try {
    resultSet.close();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }
    if (preparedStatement != null){
    try {
    preparedStatement.close();
    } catch (SQLException e){
    e.printStackTrace();
    }
    }
    if(connection != null){
    try {
    connection.close();
    } catch (SQLException e){
    e.printStackTrace();
    }
    }
    }
    return null;
    }

    private User maptoUser(ResultSet resultSet) throws SQLException {
    int id = resultSet.getInt("id");
    String username = resultSet.getString("username");
    String email = resultSet.getString("email");
    return new User(id,username,email);
    }

    @Override
    public List<User> findAll() {
    List<User> userList = new ArrayList<>();
    try {
    Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
    PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM users");
    ResultSet resultSet = preparedStatement.executeQuery();
    while (resultSet.next()){
    userList.add(maptoUser(resultSet));
    }
    } catch (SQLException e) {
    e.printStackTrace();
    }
    return userList;
    }

    @Override
    public void save(User user) {
    try {
    Connection connection = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);
    PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO users (username, email) VALUES (?, ?)");
    System.out.printf("username: %s email: %s",user.getUsername(),user.getEmail());
    preparedStatement.setString(1, user.getUsername());
    preparedStatement.setString(2, user.getEmail());
    preparedStatement.executeUpdate();
    } catch (SQLException e) {
    e.printStackTrace();
    }
    }

    @Override
    public void update(User user) {
    try {
    Connection connection = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);
    PreparedStatement preparedStatement = connection.prepareStatement("UPDATE users SET username = ?, email = ? WHERE id = ?");
    preparedStatement.setString(1,user.getUsername());
    preparedStatement.setString(2, user.getEmail());
    preparedStatement.setInt(3,user.getId());
    preparedStatement.executeUpdate();
    } catch (SQLException e){
    e.printStackTrace();
    }
    }

    @Override
    public void delete(int id) {
    try {
    Connection connection = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);
    PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM users WHERE id = ?");
    preparedStatement.setInt(1,id);
    preparedStatement.executeUpdate();
    } catch (SQLException e){
    e.printStackTrace();
    }
    }
    }
  6. 建一个简单的主程序 Main.java 来测试
    package org.example;

    import java.sql.SQLException;
    import java.util.List;

    public class DaoMain {
    public static void main(String[] args) throws SQLException {
    UserDao userDao = new UserDaoImpl();
    // 添加用户
    User user1 = new User(1,"acai","acai@qq.com");
    userDao.save(user1);
    // 查询所有用户
    List<User> userList = userDao.findAll();
    System.out.println("All Users:");
    for (User user : userList){
    System.out.println(user.toString());
    }
    // 查询指定用户
    User findUser = userDao.findById(1);
    System.out.printf("Find User: %s",findUser);
    // 更新用户
    User updateUser = new User(2,"慕容峻才","baocai.guo@qq.com");
    userDao.update(updateUser);
    // 删除用户
    userDao.delete(1);
    System.out.println("Delete user After All User:");
    for (User user : userDao.findAll()){
    System.out.println(user);
    }
    }
    }
文章作者: 慕容峻才
文章链接: https://www.acaiblog.top/Java教程/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿才的博客
微信打赏
支付宝打赏