一、基础语法
1. 数据类型
分类:
基本数据类型:
- int, short, long, byte(整数型)
- float, double(浮点型)
- char(字符型)
- boolean(布尔型)
注意: Java 中浮点数字面量默认是 double 类型,使用 float 类型需加 f / F。
引用数据类型:
- String:修饰字符串
- class:修饰类
- Interface:修饰接口

2. 基本数据类型转换
分类:
- 默认转换:小容量类型转为大容量类型
- 强制转换:大容量类型转为小容量类型(存在溢出或精度降低)
注意: 容量不是所占字节大小;如 4 个字节的 float 类型,容量大于 8 个字节的 long 类型。
容量大小排行: byte/short/char ----> int ----> long ----> float ----> double(小--->大)

3. 运算符
算术运算符

注意 i++ 和 ++i 区别:
- i++ 先取值后运算
- ++i 先运算后取值
赋值运算符

比较运算符

"=" 不是等于,是赋值
逻辑运算符

注意 && 和 & 区别:
- & 无论左边是真是假,右边表达式都运算
- && 当左边表达式为真,右边表达式才运算;若左边为假,那么右边不参与运算
- (|| 和 | 同理,|| 表示左边为真,右边不参与运算)
- 实际开发中推荐使用&& ||
位运算符

条件运算符

4. 循环语句
分类:
while 循环,do/while 循环,for 循环
用法:
- 不知道循环次数用 while 循环

- 至少循环一次用 do/while 循环

- 知道循环次数用 for 循环

5. 定义方法
- 所有方法都要定义到类里
- Java 中的方法类似其他语言中的函数,对完成某个功能的代码进行封装并为其命名,最终可以重复调用
方法定义:
例如:public static void menu( ){ ... }
- public:访问权限
- static:静态
- void:返回值类型
- menu:方法名 (自定义)
- ( ):参数列表
- { ... }:方法体
6. 数组
概述:
- 数组是一组相同数据类型的集合,是一个容器。
- 数组中可以存储基本数据类型,也可以存储引用数据类型。
- 数组本身是引用数据类型,是一个对象。
- 数组创建时必须指明长度,且长度不能改变。
- 数组中每个元素空间是连续的。
如何创建数组:
//方式 1 int[] a = new int[5]; //new:创建一个数组,并指定数组长度
//方式 2 int[] b=new int[]{1,2,3,4,5,6,7};
//方式 3 int[] c={1,2,3,4};
- 访问数组中的元素通过下标访问,即索引;
- 下标从 0 开始,是 int 类型;数组的最大索引=数组长度 -1
打印数组中元素要用到 Arrays 类:
int[] a = new int[5]; //创建一个数组,并指定数组长度
//Arrays 类:java 中提供用于操作数组的工具类,提供排序,二分查找,数组复制...
System.out.println(Arrays.toString(a));
**前言:**面向过程和面向对象都是语言设计思想,面向过程 POP 是具体的步骤,是早期的编程语言设计思想,结构简单,扩展能力差,后期维护难度较大;面向对象 OOP,面向对象设计程序时,从宏观上分析程序有哪些功能,然后对功能进行分类,把不同的功能封装在不同的类中,是一种宏观的设计,但到具体实现,仍然离不开面向过程。(二者的区别与关系)
二、面向对象
1. 类和对象
什么是类? 具有相同特征(同一类)事物的抽象描述,如人类,车类,学生类等。
类的结构:
- 变量:事物属性的描述 (名词)
- 方法:事物的行为(可以做的事情 动词)
- 构造方法:初始化对象
- 块:一段没有名称的代码块
- 内部类:即在类体中声明的类
如何写一个类?
- 第一步----发现类
- 第二步----定义类的成员变量
- 第三步----定义类的成员方法
- 第四步----使用类创建对象
/* 定义一个学生类(发现类) */
public class Student {
//定义类的成员变量
String name ;
int age;
String gender;
//定义类的成员方法
public void study(){
System.out.println("学生要好好学习");
}
public void excise(){
System.out.println("学生要多运动");
}
public static void main(String[] args) {
//使用类创建对象
Student student = new Student();
//调用类中的成员方法,成员变量
student.study();
student.name = "小王";
}
}
什么是对象? 对象是类的实例,以类为模版,在内存中创建出一个实际存在的实例。 我们使用 new 关键字来创建一个新的对象。
//类的类型 (Student 类型) 对象名 = new 类名 (Student);
Student student = new Student(); //创建一个学生对象
2. 构造方法
**分类:**构造方法分为 无参构造方法 和 有参构造方法。
特点:
- 在一个类中可以有多个构造方法 ( 构造方法可以重载 ).
- 方法名与类名相同,没有返回值,且不需要 void 修饰。
- 如果一个类没有定义构造方法,Java 会提供一个默认的无参构造方法 ; 如果一个类定义了构造方法,Java 将不会提供默认构造方法。
- 构造方法在使用 new 关键字实例化对象时自动调用。
作用:
- 构造方法的作用是初始化对象的状态,可以在构造方法中为对象的实例变量赋初值。
public class Student {
String name ;
int age;
String gender;
//无参构成方法
public Student(){
name = "小张";
age = 18;
gender = "男";
System.out.println(name+age+gender);
}
//有参构成方法
public Student(String name,int age,String gender){
this.name=name;
this.age=age;
this.gender=gender;
System.out.println(name+age+gender);
}
public static void main(String[] args) {
// 使用构造方法创建对象,并初始化
Student student = new Student(); //调用无参构造方法
Student student1 = new Student("小魏",20,"女");//调用有参构造方法
}
}
3. 方法的重载
- 在一个类中有多个名称相同的方法,如何在调用时区分同名的方法? 通过方法的参数的个数,类型,顺序;
- 方法的重载与返回值类型无关
public class Student {
String name ;
int age;
String gender;
//有参构成方法的重载
public Student(String name,int age,String gender){ }
public Student(int age,String name,String gender){//通过参数顺序区分 }
public Student(String name,int age){//通过参数个数区分 }
}
4. this 关键字
- 在一个类的内部可以使用 this 关键字来引用成员变量名,区分成员变量和局部变量。(作用)
- this 在类中表示当前正在访问的对象
//this 在类中用来表示当前正在访问的对象,this.成员变量名--显示的访问当前对象的成员变量
public Person(String name,int age,String gender){
this.name = name;
this.age = age;
this.gender = gender;
}
5. static 关键字
介绍:
- static---静态,可以修饰类中的成员变量,成员方法,代码块,内部类(不能修饰构造方法)
- 修饰成员变量 静态成员变量也称类变量,在内存中只有一份,所有对象可以共享,一般情况下,将类中所有 对象都相同的属性设置为静态的。
- 修饰成员方法 修饰的成员方法也称为类方法,可以直接使用类名访问,在静态的方法中只能访问静态的成员 变量,非静态的方法中可以使用静态的成员变量。
static 关键字修饰的属性特点:
- 随着类的加载而加载
- 优先于对象存在
- 静态成员被所有对象共享
- 可以直接使用类名访问
注意:
- 静态的方法中,只能使用静态的成员变量,因为他们都是随着类的加载而加载的;
- 一旦方法中使用了非静态的成员变量,那么此方法就不能定义为静态的;但非静态的方法中可以 使用静态的成员变量。
6. 代码块
**概念:**在类中声明的一个没有名字的代码块。
分类:
- 实例代码块:在每次创建对象时执行
{ 实例代码块内容 } - 静态代码块:在类被加载时自动执行
static { 静态代码块内容 }
类什么时候会被加载?
- 运行类中的 main 方法
- 访问类中的静态成员变量,静态成员方法
- 创建类的对象
public class Demo {
//实例代码块
{
System.out.println("1-创建对象时,不需要显示的调用,会自动执行");
}
//静态代码块
static{
System.out.println("2-在类被加载时,会自动执行");
}
//运行类中的 main 方法,此时类被加载,静态代码块执行
public static void main(String[] args) {
//创建对象,实例代码块执行
new Demo();
new Demo();
}
}

7. 访问权限修饰符
- public:公共权限--------修饰的成员,在任何地方都可以访问到。
- protected:受保护权限------在本类,同包的其他类中可以访问到,及不同包的子类。
- default:默认权限--------在本类,同包的其他类中可以访问到。
- private:私有权限 -------只能在本类访问到。
(protected 和默认 (default) 的区别就是,protected 可以在不同包的子类中访问)
8. 面向对象的三大特征
封装
- **概念:**将类中的某些信息,使用不同的访问权限修饰符隐藏起来,不让外界直接访问操作,而是通过 类中向外提供的特定方法访问,方便加入控制语句,主动权在类手中。
- 封装案例 1:将类中成员变量私有化
public class Student {
private String name;
private int age ;
}
此时在其他类中不能任意访问成员变量,只能通过类中提供的特殊的方法进行访问。
public String getName() {
return name;
}
public void setName(String name) {
//加入控制语句
if(name.length()>1&&name.length()<5){
this.name = name;
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
//加入控制语句
if(age<100&&age>0){
this.age = age;
}
}
public static void main(String[] args) {
Student s1 = new Student();
s1.setName("小王"); //符合 setName 方法中的条件
s1.setAge(-1); //不符合 setAge 方法中的条件
System.out.println(s1.getName());
System.out.println(s1.getAge());
}

封装案例 2:将类中的方法私有化
- 单例模式:让一个类在一个程序中只能创建一个对象,将类的构造方法私有化,外界不能随便用。
public class Window {
//在类加载时,只创建了一个唯一对象
static Window window = new Window();
//将构造方法私有化,在外界不能随意调用
private Window(){ }
//向外界提供可获得此对象的方法
public static Window getWindow(){
return window;
}
public static void main(String[] args) {
System.out.println(Window.getWindow());
System.out.println(Window.getWindow());
System.out.println(Window.getWindow());
}
}

可以看到,此例中创建的每个 window 的哈希值都相同于 Window 类中第一次创建的 window
- **作用:**可以有效的避免创建多个对象,达到在一个程序中只创建一个对象的作用。
继承
- **概念:**将同一类事物中共性的属性和行为进行抽取,定义为一个类中(基类),其他类可以继 承基类,就可以拥有基类中的功能,但不能使用基类中私有的内容。
- **作用:**实现代码的复用性,以及可以在子类扩展自己特有的功能,而不影响其他类。(优点)
- 使用条件:只要符合 is-a 关系(什么是什么关系,比如苹果是水果),就可以使用继承,一个类 只能直接继承一个类;而间接的可以继承多个类,称为继承的传递性,例如 b 继承 a,然后 c 继承 b,则 c 也继承了 a.
- 继承的基本语法: 在创建类的时候在类名后使用extends 关键字继承别的类,子类继承父类后,拥有了父类的成员 变量和成员方法,但不能访问私有成员。
public class Cat extends Animal{ //Cat 继承 Animal 的成员变量和成员方法,但不能访问私有成员 }
- **注意:**当一个类没有显示继承其他类的时候默认继承 object 类,Object 类是 java 类体系中最大的 类,Object 类之上再也没有别的类。

多态
**前提:**二者存在直接或间接的继承关系。
**概念:**用父类的引用变量指向子类对象,多态也称向上转型,将子类类型转为父类类型。
**作用:**用父类类型表示任意的子类类型对象,利于程序扩展。
Animal dog = new Dog();
两个时间段:
- 编译期 --- 类型是父类类型
- 运行期 --- 类型是具体的子类类型
口诀:编译看左边,运行看右边(若是静态方法则都看左边(父类))
- 多态的存在意味着可以使用父类类型的引用来调用子类对象中重写的方法。
案例:
public class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
public class Dog extends Animal{
@Override//方法的重写
public void makeSound() {
System.out.println("Dog is barking");
}
public static void main(String[] args) {
Animal dog = new Dog();//现在我们可以使用 Animal 类的引用来引用 Dog 对象
dog.makeSound(); //输出 Dog is barking
}
}
在上面的例子中,animal 引用的是一个 Dog 对象,但是调用的是 Dog 类中重写的 makeSound()方 法。这就是多态的效果!
9. 方法的重写
**概念:**当父类中方法的实现不能满足子类需求时,可以在子类中对父类的方法进行重写 ( 覆盖),这 样调用时,就会调用子类中重写的方法。
重写时需要注意:
- 子类重写的方法结构与父类方法的结果必须一致(方法名,参数列表,返回值类型必须一致)
- 子类重写的方法使用的访问权限不能小于父类被重写方法的访问权。
- 构造方法,静态方法不能重写,成员变量不存在重写。
- 使用 @Override 注解标签
了解: @ Override 是 Java 中的一个注解标签,定义在重写的方法上面,表示此方法是从父类重写而来,也可以不用添加,不过建议保留,因为编译器可以进行语法验证,并且阅读代码时可以明确的知道此方法是重写的。
10. final 关键字
- final可以修饰类,方法,参数,成员变量(常量)
- final修饰的类不能被继承,所以不能修饰抽象类,例如 Java 中 String 类就是 final 修饰
- final修饰的方法不能被重写
- final修饰方法的参数,参数值在方法中不能被改变
- final修饰的成员变量值不能改变,因此称为常量
//1.在类定义时,值就确定,直接赋值,赋值后不能改变,建议用 static 修饰
final static int A = 10;
//2.在类定义时,值不明确,必须在创建对象后在构造方法中赋值,每个对象中都有一个常量
final int COUNT ;
public demo1(){
COUNT = 10;
}
public demo1(int COUNT){
this.COUNT=COUNT;
}
(常量一般建议全部使用大写字母 多个单词间用下划线连接)
11. 抽象类
抽象方法:
- **概念:**抽象方法是一种特殊的方法,只有声明,没有具体的实现,必须用 abstract 关键字修饰,没有方法体。
- **作用:**在一些体系结构的顶端,只需要某些定义功能而没有必要实现,因为不同子类中的实现 都不同,这个时候就可以将这个方法声明为抽象方法。 例如定义一个 eat 的抽象方法:
//抽象类也需要用 abstract 来修饰
public abstract class Animal {
//抽象方法,没有方法体
public abstract void eat();
}
-
**注意:**一个类如果包含抽象方法,这个类为抽象类;一个类为抽象类,不一定有抽象方法。 抽象类也需要用 abstract 关键字修饰。
-
抽象类的特点:
- 抽象类不能用来创建对象,其他功能与正常类相同,可以有成员变量,成员方法,构造方法。
- 主要在上层定义功能,让子类继承实现。
代码实现:
abstract class Animal {
abstract void sound(); // 抽象方法
void sleep() {
System.out.println("睡觉"); // 具体方法
}
}
- 子类继承抽象类时,必须实现所有的抽象方法,否则子类也必须声明为抽象类
class Dog extends Animal {
void sound() {
System.out.println("汪汪"); // 实现抽象方法
}
}
- 抽象类可以有构造方法,但是不能被实例化。子类可以通过使用"super"关键字来调用抽象类的 构造方法和方法。
12. 接口
介绍:
- 接口可以看做是一种特殊的抽象类,里面可以包含抽象方法,但不能被创建对象;接口可以被 类实现,实现类必须实现接口中定义的所有方法;接口和抽象类十分的类似,接口内部的方法也 是默认抽象的,不可在内部实例化的,只能由接口的调用者去实现。
- 接口通过关键字 interface定义;类通过implements关键字来实现接口。
如何定义接口?
//interface 关键字修饰接口
public interface MyInterface {
int num = 10; // public static final int num = 10;
void eat(); //public abstract void eat();
//静态方法,可以直接通过接口名调用
public static void test(){
}
//默认方法,被子类继承后调用
default void test1(){
}
}
**注意:**类实现接口后,要么重写接口中所有抽象方法,要么声明为抽象类。
- 一个类可以实现多个接口:
public class MyClass implements A,B{
//Myclass 类实现 A,B 接口
}
- 一个接口可以继承多个接口:
public interface A extends B,C{
//A 继承 B,C 接口
}
三、API 常用类
Object 类
介绍:
- Object 类是所有 Java 类的祖先(根基类),每个类都使用 Object 作为超类(父类),所有对象(包括数组)都继承实现这个类的方法。
new int[10].hashCode(); //数组也继承 Object 类
- 如果在类的声明中未使用 extends 关键字指明其基类,则默认基类为 Object 类
- public class Person {...} 等价于:public class Person extends Object {...}
常用方法:
1. toString 方法
- Object 类中定义有 public String toString() 方法,其返回值是 String 类型,描述当前对象的有关息;当需要通过输出语句输出一个对象时,如 System.out.println(person),将自动调用该对象类的 toString() 方法,如果该类中没有重写 toString(),那么默认调用 Object 类中 toString(),默认输出对象 hashCode 值。我们可以根据需要在用户自定义类型中重写 toString() 方法。
public class Person {
private String name ;
private int age;
public Person(){ }
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/*
* 当输出一个对象时会默认调用此对象的 toString().
* 如果类中没有定义 toString(),会调用 Object 类中 toString(),
* Object 类中 toString() 是吧对象在内存的哈希值返回 (以 16 进制返回)
* 把对象信息通过字符串形式输出
*/
//重写 toString()
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
public static void main(String[] args) {
Person p1=new Person("小魏",18);
System.out.println(p1);
}
当我们重写 toString 方法后就可以把对象信息通过字符串形式输出了。

(重写后的结果)
如果不重写的话会默认调用 Object 类中 toString(),把对象在内存的哈希值返回 (以 16 进制返回)。

(没重写的结果)
快速生成 toString 重写方法: 右键选择 Generate,点击 toString() 即可。



2. equals 方法
- boolean equals(Object obj) 判断两个对象是否相等
注意:
- Object 类中的 equals默认比较的是两个对象的地址是否相等,我们就可以使用 == 代替它,在其他类中,一般都重写了 equals(),把它改造为比较对象中的内容是否相等
public class Test_2 {
public static void main(String[] args) {
Person p1 = new Person("小魏",16);
Person p2 = new Person("小魏",16);
System.out.println(p1==p2); //比较的是地址
System.out.println(p1.equals(p2)); //已经对 equals 方法进行了重写,使其比较的是两对象内容是否相等
}
}
(Object 中的 equals 方法默认使用==比较,比较的是对象地址,这点需要注意)
在 Person 类中对 equals 方法进行重写,使其比较的是对象中的内容是否相等
@Override
public boolean equals(Object obj) {
if(obj instanceof Person){
Person other =(Person)obj;
return name.equals(other.name) && age ==other.age;
}
return false;
}

关于 " == ":
- "==" 比较等号两边是否相等
- 当==用于基本类型比较时,比较的是变量值是否相等。
- 当==用于引用类型比较时,比较的是对象的地址是否相等。
(JDK 提供的一些类,如 String,Date 等,重写了 Object 的 equals 方法,调用这些类的 equals 方法,x.equals (y),当 x 和 y 所引用的对象是同一类对象且属性内容相等返回 true 否则返回 false)
Arrays 类
常用方法:
1. equals( ) 方法
- 比较两个数组内容是否相等,返回值为 boolean 类型。
public class Null {
public static void main(String[] args) {
/* 比较两个数组内容是否相等 */
int[] a={1,2,3,4};
int[] b={1,2,3,4};
System.out.println(Arrays.equals(a,b));
}
}

2. copyOf( ) 方法
- 数组复制,将原数组内容复制到一个指定长度新数组中。
public class Null {
public static void main(String[] args) {
int[] c=new int[3];
c[0]=0;
c[1]=1;
c[2]=2;
int[]d=Arrays.copyOf(c,6);//将原数组 c 复制到长度为 6 的新数组 d 中
//(原数组,新数组长度)
System.out.println(Arrays.toString(d));
}
}

3. fill( ) 方法。
- 用指定的值,将指定数组中的值进行填充。
public class Null {
public static void main(String[] args) {
int[]e ={1,2,3,4,5};
Arrays.fill(e,0);
System.out.println(Arrays.toString(e));
int[]f = new int[10];
Arrays.fill(f,6);
System.out.println(Arrays.toString(f));
}
}

4. sort( ) 方法
- 排序,且可通过索引局部排序
public class Null {
public static void main(String[] args) {
//全部排序
int[] a = {5,4,3,2,1};
Arrays.sort(a);
System.out.println(Arrays.toString(a));
//通过索引指定区间排序,tolndex 索引对应的值不参与排序
int[] b ={6,5,4,3,2,1};
Arrays.sort(b,1,4); //对数组 b 索引 1~4 元素排序,所有 4 不参与
System.out.println(Arrays.toString(b));
}
}

5. binarySearch( )方法
- 二分查找,查找前需要先排序
public class Null {
public static void main(String[] args) {
int[] b ={5,4,6,8,2,1,7};
Arrays.sort(b); //排序后 b={1,2,4,5,6,7,8}
int index =Arrays.binarySearch(b,6); //需要找 6
System.out.println(index); //输出索引,排序后 6 对应的索引为 4
}
}

6. toString( ) 方法
- 将数组中的元素内容拼接成一个字符串输出
public class Null {
public static void main(String[] args) {
int[] a={1,2,3,4};
System.out.println(a); //输出数组首元素地址,不是数组的元素内容
System.out.println(Arrays.toString(a));;//通过 toString() 输出元素内容
}
}

String 类
1. 获取功能的常用方法
- int length( ) 获取字符串长度
- char charAt( ) 获取指定位置上的字符
- int indexOf( ) 获取字符首次出现的位置,也可以从指定位置开始查找
- lastIndexOf( ) 从后往前找
- String substring( ) 从指定位置开始截取一个字符串副本
public static void main(String[] args) {
//获取功能
String s1 = "abcdabcd";
//int length() 获取字符串长度
System.out.println(s1.length()); // 8
//char charAt() 获取指定位置上的字符
System.out.println(s1.charAt(4)); //索引 4 对应的字符 a
//int indexOf() 获取字符首次出现的位置
System.out.println(s1.indexOf("a")); //注意是首次出现的位置
//int indexOf(str: ,fromindex: ) 从指定位置开始查找
System.out.println(s1.indexOf("a",4)); //lastIndexOf() 从后往前找
System.out.println(s1.lastIndexOf("a"));
//String substring() 从指定位置开始截取一个字符串副本
String s2=s1.substring(2,6); //截取索引 2~6 的字符串 (不包含索引 6 对应的字符)
System.out.println(s2); // 2 <= res < 6
}

2. 转换功能的常用方法
- String toUpperCase( ) 将字符串内容全部转换为大写
- String toLowerCase( ) 将字符串内容全部转换为小写
- String concat(String str) 拼接指定字符串内容到原字符串末尾
- String [ ] split(分割符) 通过分割符将字符串分割并以数组形式返回
public static void main(String[] args) {
String s1="WoShiChinese";
System.out.println(s1.toUpperCase()); //转大写
System.out.println(s1.toLowerCase()); //转小写
String s2 ="我爱中国";
System.out.println(s2.concat(s1)); //拼接 s1 字符串内容到 s2 字符串末尾
//String[] split(分割符)
String s3 = "张三:中国人;"+"Tom:英国人;"+"梅西:阿根廷人;";
String[] strings = s3.split(";"); //通过" ; "来截取 s3 字符串
System.out.println(Arrays.toString(strings));
}

3. 替换功能的常用方法
- String replace(char old,char new) 单个字符替换
- String replace(String old,String new) 整个字符串替换
- replaceAll(String regex, String replacement) 替换字符串中所有数字
- replaceFirst(String regex, String replacement) 替换字符串中第一个数字
public static void main(String[] args) {
String s1 = " ab5cd7bda";
//用"A"替换字符串中所有的"a"
System.out.println(s1.replace("a","A"));
//用"G"替换字符串中所有数字 \d 表示所有字符串中所有的数字
System.out.println(s1.replaceAll("\\d","G"));
//用"R"替换字符串中第一个数字
System.out.println(s1.replaceFirst("\\d","R"));
}

4. 去除字符串两端空格
- String trim( ) 去除字符串两端空格(字符串中间的空格不能去除)
public static void main(String[] args) {
//String trim() 去除字符串两端空格
String s2 =" abcEF 13aD1 ";
System.out.println(s2);
System.out.println(s2.length()); //原字符串长度
System.out.println(s2.trim());//去除两端空格,字符串中间的空格不能去除
System.out.println(s2.trim().length()); //去除空格后字符串的长度
}

StringBuffer 类
注意:
- String 可以直接创建字符串对象,而 StringBuffer 不能,需要新创建一个字符串对象。
String s1 = "abc";
StringBuffer s2 = new StringBuffer("abc");
添加功能
- public StringBuffer append(String str) 在原字符串末尾添加字符串
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer("abc");
s1.append("def"); // 不能使用+=
System.out.println(s1);
}
输出:abcdef
- public StringBuffer insert(int offset,String str) 向指定位置插入字符串
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer("abc");
//向字符串 s1 末尾添加"def"
s1.append("def"); // 不能使用+=
System.out.println(s1);
//向指定位置插入字符串
s1.insert(1,"ABC"); //向索引为 1 的位置插入字符串"ABC"
System.out.println(s1);
}
输出:aABCbcdef

删除功能
- public StringBuffer deleteCharAt(int index) 删除单个字符
- public StringBuffer delete(int start,int end) 删除指定区间的元素,不包含结尾
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer("abcdef");
s1.deleteCharAt(2); //删除索引为 2 的字符
System.out.println(s1);
//删除指定区间的元素,不包含结尾
StringBuffer s2 = new StringBuffer("ABCDEF");
s2.delete(1,3);
System.out.println(s2);
}

替换功能
- public StringBuffer replace(int start,int end,String str)
StringBuffer s1 = new StringBuffer("abcdef");
s1.replace(0,3,"ABC");
System.out.println(s1);
输出:ABCdef
反转功能
- public StringBuffer reverse( )
StringBuffer s1 = new StringBuffer("abcdef");
s1.reverse(); //反转
System.out.println(s1);
输出:fedcba
截取功能
- 从 StringBuffer 中截取一个副本,返回给一个新的 String 对象,StringBuffer 对象不变
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer("abcdef");
//从 StringBuffer 中截取一个副本,返回给一个新的 String 对象,StringBuffer 对象不变
String s2=s1.substring(1,4);
System.out.println(s1);
System.out.println(s2);
}

StringBuffer 和 String 的区别
- String 修饰的字符串是一个值不能改变的字符串,用 String 声明的字符串对象值一旦给定就不能改变了,每次拼接都会创建新的字符串对象,耗时且占用空间。
- StringBuffer 是内容可以改变的字符串,值可以改变且不需要创建新对象,在多任务执行时是安全的,适合单线程。
Math 类
- 可以直接调用
介绍: java.lang.Math 提供了一系列静态方法用于科学计算;其方法的参数和返回值类型一般为 double 型。
常用方法有:
- abs 绝对值
- sqrt 平方根
- pow(double a, double b) a 的 b 次幂
- max(double a, double b) min(double a, double b) 比较两数的大小
- random( ) 返回 0.0 到 1.0 的随机数
- long round(double a) double 型的数据 a 转换为 long 型(四舍五入)
public static void main(String[] args) {
//Math 类
System.out.println(Math.abs(-3)); //绝对值
System.out.println(Math.sqrt(25)); //平方根
System.out.println(Math.pow(2,5)); //次方
System.out.println(Math.round(9.35));//四舍五入
System.out.println(Math.floor(4.9)); //向下舍去
System.out.println(Math.ceil(1.1)); //向上补加
System.out.println(Math.random()); //0~1 随机生成 (不等于 1)
}

Random 类
- 不能直接调用,使用 Random 类之前需要创建新的对象。
- **介绍:**此类用于产生随机数
- 常用方法有:
import java.util.Random;
public static void main(String[] args) {
Random random = new Random();
System.out.println(random.nextBoolean()); //true 或 false
System.out.println(random.nextInt());//在 int 的取值范围内随机返回一个整数
System.out.println(random.nextLong());
System.out.println(random.nextInt(35)+1); //在给定范围内随机获取一个数 0=<res<给定数
byte[] bytes = new byte[5];
random.nextBytes(bytes); //随机取出数组长度个 byte 类型的随机数
System.out.println(Arrays.toString(bytes));
}

Date 类
- 不能直接调用,使用 Date 类之前需要创建新的对象。
1. 获取程序运行时刻的时间
Date date = new Date(); //获取程序运行时刻的时间
System.out.println(date);
2. 获取自 1970 1.1 0:0:0 到程序运行时刻的毫秒值
Date date = new Date(); //获取的是自 1970 1.1 0:0:0 到程序运行时刻的毫秒值
System.out.println(date.getTime());
3. 测试效率
//测试效率
Date date1 = new Date();
System.out.println(date1.getTime()-date.getTime());
测试效率
- 测试 String 字符串和 StringBuffer 字符串循环拼接 10 万次,程序运行耗时。
- 测试 String 字符串拼接速度。
public static void main(String[] args) {
//获取程序运行时刻的时间
Date date = new Date();
// System.out.println(date.getTime());
String s1 = "w";
// StringBuffer s2 = new StringBuffer("w");
String;
// StringBuffer s4 =new StringBuffer("");
for (int i = 0; i < 100000; i++) {
s3 = s3.concat(s1);
}
//获取下一个程序运行时刻的时间,即上一个程序运行结束时刻的时间
Date date1 = new Date();
// System.out.println(date1.getTime());
System.out.println("用时"+(date1.getTime()-date.getTime())+"毫秒");//最终运行时间
}

- 测试 StringBuffer 字符串拼接速度。
public static void main(String[] args) {
//获取程序运行时刻的时间
Date date = new Date();
// System.out.println(date.getTime());
//String s1 = "w";
StringBuffer s2 = new StringBuffer("w");
//String;
StringBuffer s4 =new StringBuffer("");
for (int i = 0; i < 100000; i++) {
s4 = s4.append(s2);
}
//获取下一个程序运行时刻的时间,即上一个程序运行结束时刻的时间
Date date1 = new Date();
// System.out.println(date1.getTime());
System.out.println("用时"+(date1.getTime()-date.getTime())+"毫秒");//最终运行时间
}

在前文中我们了解了二者的区别,通过这次测试我们也验证了之前的结论:用 String 声明的字符串对象值一旦给定就不能改变了,每次拼接都会创建新的字符串对象,耗时且占用空间;而 StringBuffer 是内容可以改变的字符串,值改变的同时不需要创建新对象,所有拼接起来速度更快。
四、集合
前言:
- 在开发实践中,我们需要一些能够动态增长长度的容器来保存我们的数据,java 中为了解决数据存储单一的情况,java 中就提供了不同结构的集合类,可以让我们根据不同的场景进行数据存储的选择,如 Java 中提供了 数组实现的集合,链表实现的集合,哈希结构,树结构等。
体系图
Java 中的集合体系如图:

- **分类:**集合可以分为单列集合和双列集合。
单列集合
-
Collection 接口定义了单列集合共有的方法,其子接口Set和List分别定义了存储方式。
-
List 接口继承了 Collection 接口,有三个实现的类,分别是:ArrayList (数组列表) | LinkedList (链表列表) | Vector 数组列表 (且线程安全)。
-
Set 接口继承了 Collection 接口,有两个实现的类,分别是:HashSet | TreeSet。
-
区别:
- **List:**可以有重复元素
- **Set:**不可以有重复元素
List 接口及实现类
ArrayList
- 底层有一个数组,可以动态扩展数组长度,并提供了一系列方法操作 ; 查询快,在中间增加 / 删除慢。
常用方法:
ArrayList<String> s=new ArrayList<>();
s.add("a");
s.add("b");
s.add("c");
s.add("d");
System.out.println(s.get(2));//根据索引得到指定位置的元素
s.remove(0);//删除并返回指定位置的元素
System.out.println(s);
s.set(0,"X");//替换指定元素并返回数组
System.out.println(s);
s.size();
System.out.println(s.size());//返回实际元素个数
s.addFirst("X");
s.addLast("X");
LinkedList
- 底层是一个链表结构 ; 查询慢,但增加 / 删除元素快。(特点)
我们发现 ArrayList 和 LinkedList 的特点正好相反,原因如图:

Vector
- 和 ArrayList 一样,底层也是数组实现,不同的是 Vector 的方法默认加了锁**,线程是安全的**。

【常用方法】
迭代器遍历 ( Iterator )
List集合遍历有三种方式:for 循环,增强 for 循环和迭代器。
- 增强 for 循环 遍历元素时,不允许修改集合元素 (删除,添加)
- for 循环 是允许操作 (删除) 元素的,但要注意索引的变化与元素位置的移动。
- 而使用迭代器即可以修改集合元素,且不用担心操作元素时索引的变化与元素位置的移动 (优点)
Iterator
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
//获得集合对象的迭代器对象
Iterator<String> it = arrayList.iterator();
while (it.hasNext()){
String s = it.next();//获取到下一个元素
if(s.equals("a")){
it.remove();//使用迭代器对象删除元素
}
}
ListIterator
- ListIterator 迭代器 只能对 List 接口下的实现类遍历。(条件)
- 从指定的位置开始向前或者向后遍历
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
//ListIterator 迭代器 只能对 List 接口下的实现类遍历,
//listIterator(index); 可以从指定的位置开始向前或者向后遍历
ListIterator<String> listIterator = arrayList.listIterator(2);
while (listIterator.hasNext()){
System.out.println(listIterator.next());
}
}
- 逆序遍历
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("a");
arrayList.add("b");
arrayList.add("c");
arrayList.add("d");
ListIterator<String> listIterator = arrayList.listIterator(arrayList.size()); //4 3 2 1 0
while (listIterator.hasPrevious()){ //获取上一个元素
System.out.println(listIterator.previous());
}
}
Set 接口及实现类
- Set 中所储存的元素是不重复的,无序的,且 Set 中的元素没有索引。
- 由于 Set 中元素无索引,所有其实现类中没有 get() [通过索引获取指定位置元素] 且不能通过 for 循 环进行遍历
HashSet
- HashSet 是一个不允许有重复元素的集合,是无序的,不是线程安全的。
public static void main(String[] args) {
HashSet set =new HashSet<>();
set.add("a");
set.add("a");
set.add("b");
set.add("c");
//元素是不重复的
System.out.println(set);//输出:[a,b,c]
HashSet set1 =new HashSet<>();
set1.add("c");
set1.add("s");
set1.add("x");
set1.add("d");
//元素是无序的
System.out.println(set1);//输出:[c,s,d,x]
}
★ HashSet 在添加元素时,是如何判断元素重复的?(面试高频题)
- 在底层会先调用 hashCode(),注意,Object 中的 hashCode() 返回的是对象的地址,此时并不会调用;此时调用的是类中重写的 hashCode(),返回的是根据内容计算的哈希值,遍历时,会用哈希值先比较是否相等,会提高比较的效率;但哈希值会存在问题:内容不同,哈希值相同;这种情况下再调 equals 比较内容,这样既保证效率又确保安全。
TreeSet
- TreeSet 可以根据值进行排序,底层使用了树形结构,树结构本身就是有序的。
TreeSet<Integer> treeSet =new TreeSet<>();
treeSet.add(2);
treeSet.add(1);
treeSet.add(4);
treeSet.add(4);
treeSet.add(3);
System.out.println(treeSet);//输出 [1,2,3,4]
- 向树形结构中添加元素时,如何判断元素大小以及元素是否重复?
- 向 TreeSet 中添加的元素类型必须实现 Comparable 接口,重写 compareTo() ; 每次添加元素时,调 用 compareTo() 进行元素大小判断 (小于 0 放左子结点,等于 0 表示重复,大于 0 放右子节点)
- TreeSet 集合的遍历只能通过 增强 for 循环 和 迭代器 (Iterator) 遍历。(元素没有索引)
双列集合
Map 接口
Map 接口共性:
- 数据存储是以 ( 键,值 ) 形式存储
- 键不能重复,值可以重复。
- 通过键找到值,一个键只能映射到一个值。
HashMap
- HashMap 中的键是无序的
//可以存储两组值 (键 K,值 V)
HashMap<String,String> map =new HashMap<>();
map.put("a","aa"); //put() 向 map 中添加一组键 值对
map.put("w","ww");
map.put("c","cc");
map.put("s","ss");
map.put("a","aaa"); /* 替代之前的键 a */
System.out.println(map); //键是无序的
输出:{a=aaa, c=cc, s=ss, w=ww}
- HashMap 中的常用方法
//常用方法
HashMap<String,String> map =new HashMap<>();
map.put("a","aa"); //put() 向 map 中添加一组键 值对
map.remove("a"); //删除指的的键,返回对应的值
map.clear(); //清空键值对
map.isEmpty(); //判断键值对的个数是否为空
map.containsKey("a"); //是否含对应键
map.containsValue("aaa");//是否含对应值
map.get("s"); //传键返值
map.size(); //有几组键值对
★ HashMap 底层存储数据的结构:(面试高频题)
- 底层使用了一个长度默认为16的哈希数组,用来确定元素的位置,每次用 key 计算出哈希值,再 用哈希值%数组长度确定元素位置,将元素放在哈希表中指定的位置。
- 后来继续添加元素,如果出现位置相同且不重复的元素,那么将后来元素添加到之前元素的 next 节点。
- 当链表长度等于8且哈希数组的长度大于64时链表会自动转为红黑树。
补充: 哈希表负载因子为0.75, 当哈希表使用数组的 0.75 倍时会自动扩容为原来数组长的 2 倍。

TreeMap
- 底层使用树形结构存储键值
- 键可以排序
- 键元素类型必须实现 Comparable 接口,重写 compareTo()
Hashtable
- 底层实现也是用到 key 的哈希值计算位置判断元素是否重复
- 方法上都添加了synchronized
HashMap 和 Hashtable 的区别:
- Hashtable 中不能存储为 null 的键和为 null 值,但 HashMap 中可以。
HashMap<Integer,String> map =new HashMap<>();
map.put(1,"a");
map.put(2,null);
map.put(null,null);
System.out.println(map); //输出:{null=null, 1=a, 2=null}
Hashtable<String,String> table =new Hashtable<>();
table.put(null,"a"); //报错
System.out.println(table);
Map 集合遍历
//可以存储两组值 (键 K,值 V)
HashMap<String,String> map =new HashMap<>();
map.put("a","aa"); //put() 向 map 中添加一组键 值对
map.put("w","ww");
map.put("c","cc");
map.put("s","ss");
map.put("a","aaa"); /* 替代之前的键 a */
System.out.println(map); //键是无序的
//遍历方式 2:(推荐)
Set<Map.Entry<String,String>> entries =map.entrySet();
for (Map.Entry entry:entries){
System.out.println(entry.getKey()+":"+entry.getValue());
}
//遍历方式 1:拿到所有的键
Set<String> keyset =map.keySet();
for (String key:keyset){
System.out.println(key+":"+map.get(key));
}
Collections 类
**概述:**Collections 是集合类的工具类,与数组的工具类 Arrays 类似。
常用方法:
-
sort(Comparator<? super E>):void List
对集合中的元素排序。
-
reverse(List<?>):void
反转集合中的元素。
-
shuffle(List<?>):void
打乱元素中的元素。
-
fill(List<? super T>,T):void
用 T 元素替换掉集合中的所有的元素。
-
copy(List<? super T>,List<? extend T>):void
复制并覆盖相应索引的元素
-
swap(List<?>,int,int):void
交换集合中指定元素索引的位置.
-
replaceAll(List,T,T):boolean
替换成指定的元素。
代码演示:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class collections {
public static void main(String[] args) {
ArrayList<Integer> list =new ArrayList<>();
list.add(1);
list.add(2);
/* addAll(Collection<? super T> c, T... elements); */
Collections.addAll(list,3,4,5,6);//将指定的可变长度参数添加到指定集合中
System.out.println(list);
Collections.sort(list); //排序 (默认升序)
System.out.println("升序:"+list);
//Collections.binarySearch() 二分查找
System.out.println(Collections.binarySearch(list,5));//二分查找
//创建了一个实现 Comparator 接口的匿名内部类对象,省去了创建一个类简化语法
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.intValue()- o1.intValue(); //降序
}
});
System.out.println("降序:"+list);
ArrayList<Integer> list1 =new ArrayList<>();
Collections.addAll(list1,1,2,3,4);
Collections.fill(list1,5);
System.out.println(list1);
Collections.replaceAll(list1,,);
}
}
五、IO 流
File 类
知识概要:
- 一个 File 类的对象可以表示一个具体的文件或目录。
- File 对象可以对文件或目录的属性进行操作,如:文件名、最后修改日期、文件大小等。
- File 对象无法操作文件的具体数据,即不能直接对文件进行读/写操作
File 的构造方法:
- File(String pathname)指明详细的文件或目录的路径
//指明详细的路径以及文件名
File file =new File("E:/demo1.txt");
//指明详细的路径以及目录名
File directory =new File("E:/temp");
//注意使用/或\来区分路径等级
File 类的常用方法:

注意:delete 删除一个文件夹时,文件夹中必须是空的
使用 File 类创建文件及目录: ① 创建文件
//指明详细的路径以及文件名
File file =new File("E:/demo3.txt");
if (!file.exists()){//判断指向文件是否存在
file.createNewFile();//通过 createNewFile() 方法创建文件
}
file.delete();//删除文件
② 创建文件夹(单级文件夹和多级文件夹)
mkdir( ) 创建单级文件夹 mkdirs( ) 创建多级文件夹
//指明详细的路径以及目录名
File directory =new File("E:/demo");
directory.mkdir();//mkdir 用来创建单级文件夹
File directorise =new File("E:/demo/demo1/demo2");
directorise.mkdirs();//mkdirs 用来创建多级文件夹
如何得到一个目录中的所有文件: 思路:对目录进行遍历,遇到文件直接输出,遇到子目录再对子目录遍历,直到遇见文件
import java.io.File;
import java.io.IOException;
public class Review1 {
public static void main(String[] args) throws IOException {
//指明详细的路径以及目录名
File directory =new File("E:/IO");
search(directory);
}
//创建一个方法用来遍历目录里的文件
public static void search(File file){
File[] files =file.listFiles();////获取到当前给定目录的子级文件/文件夹
for (File f:files){
if (f.isFile()){//若是文件,直接输出
System.out.println(f);
}else {
search(f);//若是目录,继续向下查找,直到遇到文件
}
}
}
}
输入 (I) 与输出 (O)
- 输入和输出是一个相对概念,输入和输出是相对于我们的程序。
- 输入---Input 把电脑硬盘上的数据读到程序中,称为输入,即 input,进行数据的 read 操作。
- 输出---Output 从程序往外部设备写数据,称为输出,即 output,进行数据的 write 操作。

如图起连接作用的通道也就是我们通常所说的流。
字节流与字符流
体系图

知识概要:
- 字节流:读取时以字节为单位,可以读取任意文件。
- 字符流:读取时以字符为单位,只能读取文本文件。
字节流中常用类:
- 字节输入流 InputStream
- 字节输出流 OutputStream
字符流中常用类:
- 字符输入流 Reader
- 字符输出流 Writer
常用类的基本方法
InputStream 和 OutputStream 的子类都是字节流,可以读写二进制文件,主要处理音频、图片、歌曲、字节流处理单元为 1 个字节。
InputStream 的基本方法:
- 读取一个字节并以整数的形式返回 (0~255),如果返回 -1 已到输入流的末尾。
- int read() throws IOException
- 读取一系列字节并存储到一个数组 buffer,返回实际读取的字节数,如果读取前已到输入流的末尾返回 -1
- int read(byte[] buffer) throws IOException
- 关闭流释放内存资源
- void close() throws IOException
OutputStream 的基本方法:
- 向输出流中写入一个字节数据,该字节数据为参数 b 的低 8 位
- void write(int b) throws IOException
- 将一个字节类型的数组中的从指定位置(off)开始的 len 个字节写入到输出流
- void write(byte[] b, int off, int len) throws IOException
- 关闭流释放内存资源
- void close() throws IOException
Reader 和 Writer 的子类都是字符流,主要处理字符或字符串,字符流处理单元为 1 个字符。
Reader 的基本方法:
- 读取一个字符并以整数的形式返回,如果返回 -1 已到输入流的末尾。
- int read() throws IOException
- 读取一系列字符并存储到一个数组 buffer,返回实际读取的字符数,如果读取前已到输入流的末尾返回 -1。
- int read(char[] cbuf) throws IOException
- 关闭 void close() throws IOException
Writer 的基本方法:
- 向输出流中写入一个字符数据,该字节数据为参数 b 的 16 位
- void write(int c) throws IOException
- 将一个字符类型的数组中的从指定位置(offset)开始的 length 个字符写入到输出流
- void write(char[] cbuf, int offset, int length) throws IOException
- 关闭 void close() throws IOException
字节流读写文件
① 首先在 D 盘中创建一个 ddd.docx 文件 (9.86KB),再在 E 盘中创建一个 eee.docx 文件 (空)


② 代码实现:
public static void main(String[] args) throws IOException {
FileInputStream inputStream1 =new FileInputStream("D:/ddd.docx");//输入流
FileOutputStream outputStream1 =new FileOutputStream("E:/eee.docx");//输出流
int a =0 ;
while((a=inputStream1.read())!=-1){//只要 a!=-1 说明还未读完
outputStream1.write(a);//写内容 (以字节为单位)
}
//关闭通道,否则文件一直处于打开 (占用) 状态
inputStream1.close();
outputStream1.close();
}
③ 运行程序,可以发现我们成功将 ddd.docx 文件内容写到了 eee.docx 文件中 (0 字节➝9.86KB)

每次读写完后记得关闭通道,否则则文件一直处于打开 (占用) 状态。
高效文件读写: 创建一个 byte 数组,一次读 byte 数组长度个字节,便于提高读写效率。
public static void main(String[] args) throws IOException {
FileInputStream inputStream =new FileInputStream("D:/demo1.txt");
FileOutputStream outputStream=new FileOutputStream("E:/IO.txt");
byte[] bytes =new byte[10];
//read(bytes) 一次读 byte 数组长度个字节,文件内容读取完后返回 -1
//size:每次实际往数组中装入的元素的个数
int size =0;
while((size=inputStream.read(bytes))!=-1){
//一次向外写出一个 byte 数组长度个字节内容,从指定位置开始写,写 size 个
outputStream.write(bytes,0,size);
}
inputStream.close();
outputStream.close();
}
字符流读写文件
字符流只能读取文本文件
① 我们先创建一个 char1.txt 文本,并写入内容进行读写测试

② 通过代码实现将 char1.txt 文本的内容读写到 char2.txt 文本
public static void main(String[] args) throws IOException {
FileReader reader =new FileReader("E:/char1.txt");
FileWriter writer =new FileWriter("E:/char2.txt");
int b = 0;
while((b=reader.read())!=-1) {
System.out.println(b);//打印字符编码
writer.write(b);
}
reader.close();
writer.close();
}
字符流读取时以字符为单位,会将读到字节结合编码表转换为一个字符编码
③ 我们可以将每次读取到的字符对应的编码打印出来

④ 成功读写内容到 char2.txt 文本

上述写法的弊端: 每次运行程序会将之前所读写的内容覆盖带掉,不能做到在原内容的基础上续写
解决方法:在字符输出流的对象路径后加上true, 表示可续写。

此时当我们多次运行程序时,发现之前的所读写的内容依然存在

那么我们怎样进行换行操作?
public static void main(String[] args) throws IOException {
FileReader reader =new FileReader("E:/char1.txt");
//保留原来的内容,在原内容基础上向后追加 (续写)
FileWriter writer =new FileWriter("E:/char2.txt",true);
BufferedReader bufferedReader =new BufferedReader(reader);
BufferedWriter bufferedWriter =new BufferedWriter(writer);
String line = null ;
while((line=bufferedReader.readLine())!=null){//只要每次读取不为空,则读取一行
bufferedWriter.write(line);//写一行
bufferedWriter.newLine();//插入换行符
}
bufferedReader.close();
bufferedWriter.flush();
bufferedWriter.close();//记得在读写完毕后关闭通道
}
注意: 由于读取一行的方法 readLine( ) 在 BufferedReader 中,换行方法 newLine( ) 在 BufferedWriter 类中,所以需要用到缓冲字符输入输出流。
此时当我们多次运行程序时,会将每次读取到的内容进行换行读写,便于记录数据。

节点流与处理流
按封装类型流又分为:
- 节点流:直接封装的是文件,数据。
- 处理流:封装的是其他节点流对象;可以提供缓冲功能,提高读写效率。
节点流中常用类:
- 字节输入流 FileInputStream
- 字节输出流 FileOutputStream
- 字符输入流 FileReader
- 字符输出流 FileWriter
处理流中常用类:
- 缓冲字节输出流 BufferedOutputStream
- 缓冲字节输入流 BufferedInputStream
- 缓冲字符输入流 BufferedReader
- 缓冲字符输出流 BufferedWriter

代码演示:
public static void main(String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream("D:/demo1.txt"); //封装的是一个节点流对象,可以提供缓冲功能,称为处理流/包装流
BufferedInputStream bufferedInputStream =new BufferedInputStream(inputStream,20);
FileOutputStream outputStream =new FileOutputStream("E:/IO.txt");
BufferedOutputStream bufferedOutputStream =new BufferedOutputStream(outputStream,20);
int size =0;
byte[] bytes =new byte[10];
while ((size=bufferedInputStream.read(bytes))!=-1){
bufferedOutputStream.write(bytes,0,size);
}
bufferedInputStream.close();
bufferedOutputStream.close();
}
缓存区流底层代码:

对象输入输出流 ( 序列化 )
基础理论知识
怎么理解对象输入输出流? 把 java 中的对象输出到文件中,从文件中把对象输入到程序中。
为什么要这样做 (目的)? 当我们创建一个对象时,如 new Student("小张",20); 数据存储在对象中,对象是在内存中存储的,一旦程序运行结束,对象就会销毁,有时需要将对象的信息长久保存,就需要将对象输入到文件中。 (例如系统升级,关闭服务器时将对象保存起来,升级完毕后再重新把数据还原回来.)
对象的序列化和反序列化:
- 把对象输入到文件的过程也称为对象的序列化。
- 把对象从文件输入到程序的过程称为对象的反序列化,反序列化时会生成一个新的对象,所以反序列化也是创建对象的一种方式。
注意: 当一个类的对象需要被序列化到文件时,这个类必须要生成一个序列化编号。
- 如果一个类需要被序列化到文件中,那么这个类就需要实现 Serializable 接口,实现后,会自动的为该类生成一个序列化编号。
关于序列化编号的意义:
- 编号是类的唯一标识,但是自动生成的编号在类信息改变后会重新为类生成一个编号。
- 可以在类中显示的生成一个编号,这样类信息修改后,编号也不会改变。
常用类及基本方法:
- 对象的输出流:ObjectOutputStream
- 对象的输入流:ObjectInputStream
- 在 ObjectInputStream 中用 readObject() 方法可以直接读取一个对象。
- 在 ObjectOutputStream 中用 writeObject() 方法可以直接将对象保存到输出流中。
生成序列化 ID 教程
如何在 IDEA 中设置,使可以在类中生成序列化 ID?(教程)


设置成功后,当我们把鼠标移至类名处,点击 serialVersionUID 即可生成编号


代码实践与测试
① 我们首先创建一个学生类,需要将学生信息序列化到文件中,切记需要实现 Serializable 接口。
import java.io.Serializable; //如果一个类需要被序列化到文件中,那么这个类就需要实现 Serializable 接口
public class Student implements Serializable {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
② 进行序列化操作,将对象数据输出到文件中,使对象的信息可以做到持久化。
//对象输出 (对象的序列化)
Student student =new Student("小魏",20);//创建对象
FileOutputStream fileOutputStream = new FileOutputStream("E:/obj.txt");
ObjectOutput output = new ObjectOutputStream(fileOutputStream);
output.writeObject(student);
output.flush();
output.close();
③ 运行程序后发现,对象信息保存在了文件 obj.txt 中 (格式为乱码,但不影响最后反序列化输入结果)

④ 进行反序列化操作,将之前保存在文件 obj.txt 的对象信息输入到程序中。
public static void main(String[] args) throws IOException, ClassNotFoundException {
//对象输出 (对象的序列化)
/* Student student =new Student("小魏",20);//创建对象
FileOutputStream fileOutputStream = new FileOutputStream("E:/obj.txt");
ObjectOutput output = new ObjectOutputStream(fileOutputStream);
output.writeObject(student);
output.flush();
output.close(); */
//对象输入 (对象的反序列化)
FileInputStream inputStream = new FileInputStream("E:/obj.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Student student = (Student) objectInputStream.readObject();//强制类型转换
System.out.println(student);//打印对象信息
objectInputStream.close();//记得关闭流通道
}
运行结果:

六、异常处理机制
**本章学习内容:**使用异常处理机制,对程序运行过程中出现的异常情况进行捕捉并处理。
1. Java 异常概述
异常的概念:
- 程序在运行过程中出现的不正常情况; 例如用户输入数据有问题,读写文件时文件被强制删除了,网络传输过程中突然断网...
- 出现异常后,会导致 jvm(虚拟机) 停止运行,后续程序无法执行。
注意:
- 异常指的并不是语法错误。(语法错误,编译不通过,不会产生字节码文件,根本不能运行)
异常的抛出机制: java 中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出; 然后程序员可以捕获到这个异常对象,并处理; 如果没有捕获这个异常对象,那么这个异常将会 导致程序终止。
java 中默认的异常处理机制:
- 将出现的异常,按不同的类型分类,为每种异常封装了一个类来进行标识。
- 当出现某种类型的异常情况时,会抛出此类的对象,然后终止虚拟机的运行。
异常信息:
- 异常的类型 (在哪种情况下出现,结合 API 定位)
- 异常原因
- 异常位置

2. Java 异常体系结构

java.lang.Throwable : 异常体系的超类
Error 错误:
- 是虚拟机和 Java 代码无法解决的问题,例如虚拟机内部异常,内存不够用了。
- 堆溢出:OutOfMemoryError
- 栈溢出:StackOverflowError
Exception 异常:
- 这类异常时可以通过异常处理机制进行处理的一般性问题。
3. 常见的异常
ArithmeticException 算术异常 ArrayIndexOutOfBoundsException 数组索引越界 StringIndexOutOfBoundsException 字符串索引越界 ClassCastException 类型转换异常 NumberFormatException 数字格式化异常 NullPointerException 空指针异常
代码演示:
public static void main(String[] args) {
/* int a = 10;
int b =0 ;
System.out.println(a/b);
System.out.println("*****"); */
//ArithmeticException 算术异常
/* int[] c=new int[2];
c[3] = 0; */
//ArrayIndexOutOfBoundsException 数组索引越界
/* String s = "abc";
s.charAt(4); */
//StringIndexOutOfBoundsException 字符串索引越界
/* Object o = new Integer(10);
String so = (String) o; */
//ClassCastException 类型转换异常
/* Integer.parseInt("abc"); */
//NumberFormatException 数字格式化异常
/* String ss = null;
ss.length(); */
//NullPointerException 使用 null 中的方法称为空指针异常
}
4. 异常处理
- Java 中使用异常处理机制为程序提供了错误处理的能力
- 在编码时,就针对可能出现问题的代码,预先编写一些处理机制,当程序运行出现异常时执行处理机制,之后继续执行后续的程序。

- Java 的异常处理是通过 5 个关键字来实现的:try、catch、 finally、throw、throws

try{...}catch(){...} try{ 编写可能出现异常的代码 }catch(异常类型){ 处理机制 }
代码演示:
public static void main(String[] args) {
int a =10;
int b = 0;
try{
int c = a/b;
}catch (ArithmeticException aindex){
aindex.printStackTrace(); //打印异常信息到控制台
/* 一般在开发调试阶段使用,供开发人员定位异常问题 */
System.out.println("算术异常");
}
}
运行结果:

一个 try 代码块可以对应多个 catch
public static void main(String[] args) {
try {
String s =null;
s.length();
int num =Integer.parseInt("1a");
int[] b =new int[5];
for (int i = 0; i <=b.length ; i++) {
System.out.println(b[i]);
}
} catch (ArrayIndexOutOfBoundsException aindex) {
aindex.printStackTrace();//打印异常信息
System.out.println("数组索引越界,越界索引:"+aindex.getMessage()); //getMessage() 得到异常原因
}catch (NumberFormatException nex){
nex.printStackTrace();
System.out.println("数字格式化异常:"+nex.getMessage());
}catch (Exception e){//在具体捕获类型后 catch Exception,可以捕获任意类型,但是必须放在最后面
e.printStackTrace();
System.out.println("系统繁忙~请稍后再试!");
}
System.out.println("继续执行后续程序");
}
当我们不确定捕获类型时可以使用 Exception; Exception 可以捕获任意类型,但是需要放在具体捕获类型后 catch,否则放到首部会覆盖其他具体的捕获类型。
finally{...} finally 块中的内容总是会执行的,且只能有一个 finally 语句。 try{ 编写可能出现异常的代码 }catch(异常类型){ 处理机制 }finally{ 代码总能执行 }
使用 finally 的两种情形:
- 当 catch 错误,异常没有被捕获到,后面的代码无法执行,但 finally 中的代码是可以执行的。
public static void main(String[] args) {
int[] a = new int[5];
try {
for (int i = 0; i <= a.length; i++) {
System.out.println(a[i]);
}
}catch (NumberFormatException nex){
nex.printStackTrace();
System.out.println("数字格式化异常:"+nex.getMessage());
}finally {
System.out.println("继续执行后续程序");
}
}
- 确保在出现异常的情况下,依然最终把流对象关闭掉。
public static void main(String[] args) throws IOException {
FileInputStream inputStream =null;
try{
inputStream =new FileInputStream("F:/demo.txt"); //文件若找不到,就会出现异常
}catch (FileNotFoundException e){
e.printStackTrace();
System.out.println("文件找不到异常");
}finally {
//确保在出现异常时,仍然可以把流对象关闭掉
if (inputStream!=null){
inputStream.close();
}
}
}
七、网络编程
1. 网络编程概述
什么是计算机网络? 把分布在不同地理区域的计算机设备,通过物理线路连接起来,最终实现数据传输,资源共享。

什么是网络编程? 在网络的基础上,开发的程序能够进行数据传输。 java 语言是支持网络的,并且将网络连接的细节都封装起来了,对外提供一套网络库,就可以进行统一环境的网络编程
要进行网络数据传输的核心问题? 如何找到网络世界中的目标主机和程序?(IP 和端口) 找到后如何高效安全的进行数据传输?(协议) OK 那就让我们带着这两个问题来一探究竟吧!
2. 网络模型
理论有七层网络模型-----现实中是四层模型

应用层 —— 内容 传输层 —— 加入协议控制 网络层 —— IP 物理层 —— 网线 光纤
网络通信的三要素:IP 端口号 通信协议
IP:
- IP 地址是指互联网协议地址,可以理解为计算机的地址;如 ipconfig 局域网 IP 192.168,本机回环地址 127.0.0.1 (访问自己电脑)
端口号:
- 计算机运行的每个程序都对应分配一个整数的编号,不能重复;范围:0
65535;一般 01024 已经被系统使用或保留,例如 MySQl 的端口号是 3306.
端口对应程序 IP 对应设备
通信协议:
- 是一种规范和约定,计算机网络中实现通信必须有一些约定,即通信协议;对速率,传输代码,代码结构,出错后如何应对...等制定规则。
3. TCP 协议
Transmission Control Protocol 传输控制协议 安全可靠,但传输效率低
① 客户端向服务器端发送数据前,首先要建立连接(测试网络是否通畅) 三次握手
- 客户端向服务器发送连接请求
- 当服务器接收到客户端连接请求后,给客户端做出一个回应
- 客户端为服务器的回应再次做出确认回应

② 正式传输数据
③ 断开时需相互确认 四次挥手
- 客户端向服务器发送一个断开请求
- 服务器向客户端做出一个回应
- 服务器端把没有传完的数据传输完毕,再向客户端做出回应
- 客户端向服务器端的回应做出回应

断开...
4. UDP 协议
User Datagram Protocol 用户数据协议
- 把要发送的数据封装成一个数据包;数据包包含数据、对方的 IP、对方端口。
- 只管发送即可,是否发送成功是不知道的 (由于没有建立连接,故发送信息成功与否不晓得) *优点:*不需要建立双方连接,传输速度快。 *缺陷:*由于没有建立连接,它是不安全的。
5. TCP 网络编程
搭建服务器
- new ServerSocket(端口)
- ServerSocket 中的 accept() 方法来监听客户端的连接(此时程序阻塞)
try {
//创建服务器对象
ServerSocket serverSocket =new ServerSocket(8888);//(端口)
System.out.println("服务器启动成功!");
//监听客户端是否发送链接请求服务器
Socket socket= serverSocket.accept();//等待监听客户端的连接
System.out.println("有客户连接到了服务器!");
} catch (IOException e) {
e.printStackTrace();
}
搭建客户端
- new Socket(服务器 IP,端口)
try {
//创建客户端
Socket socket = new Socket("127.0.0.1",8888);// ("服务器的 IP",端口)
} catch (IOException e) {
e.printStackTrace();
}
后续的操作就需要用到 IO 流了 在客户端输出一个字符串,并转换为字节的形式写入程序
public static void main(String[] args) {
//创建客户端
try {
Socket socket=new Socket("127.0.0.1",8888);//服务器的 IP 端口
String s="你好世界";
OutputStream outputStream=socket.getOutputStream();
outputStream.write(s.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
在服务器输入客户端输出的字节,并将其转为字符串进行打印
public static void main(String[] args) {
//创建服务器对象
try {
ServerSocket serverSocket =new ServerSocket(8888);
System.out.println("服务器启动成功!");
//监听客户端是否发送链接请求服务器
Socket socket = serverSocket.accept();//监听客户端的连接
System.out.println("有客户端连接到了服务器");
//后续操作...
InputStream inputStream=socket.getInputStream();
byte[] bytes =new byte[100];
int size =inputStream.read(bytes);
String s =new String(bytes,0,size);
System.out.println(s);
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败,端口已被占用");
}
}
进阶:实现单线程聊天 我们通过 while 循环和 Scanner 控制台输入实现服务器与客户端交替聊天 我们使用包装流 DataInputStream,DataOutputStream 来输入输出,可以直接读到一个字符串,无需进行字符串与字节的转换。
/* 服务器 */
static Scanner scanner =new Scanner(System.in);
public static void main(String[] args) {
try {
//创建服务器
ServerSocket server = new ServerSocket(20049);
System.out.println("服务器启动成功!");
//监听客户端的连接
Socket socket = server.accept();
System.out.println("有客户端连接到了服务器!");
//服务器接收客户端的消息
while (true) {
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
String string = inputStream.readUTF();
System.out.println("服务器:" + string);
//向客户端发送消息
System.out.println("---->客户端:");
String str = scanner.next();
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
dataOutputStream.writeUTF(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/* 客户端 */
static Scanner scanner =new Scanner(System.in);
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 20049);
while (true) {
System.out.println("--->服务器:");
String s = scanner.next();
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
output.writeUTF(s);
DataInputStream inputStream = new DataInputStream(socket.getInputStream());
String string = inputStream.readUTF();
System.out.println("服务器:" + string);
}
} catch (IOException e) {
e.printStackTrace();
}
}
补充: 本机回环地址:127.0.0.1 获取服务器的 IP(ipconfig 局域网 IP 以 192.168 开头)

八、线程
1. 区分程序、进程、线程
**程序:**为实现某种功能,使用计算机语言编写的一系列指令的集合。 指的是静态的代码(安装在电脑上的那些文件)
**进程:**是运行中的程序(如运行中的原神)进程是操作系统进行资源分配的最小单位。
**线程:**进程可以进一步细化为线程,是进程中一个最小的执行单元,是 cpu 进行调度的最小单元 例如:QQ 中的一个聊天窗口
进程和线程的关系: ⑴ 一个进程中可以包含多个线程(一个 QQ 程序可以有多个聊天窗口) ⑵ 一个线程只能隶属于一个进程(QQ 的聊天窗口只能属于 QQ 进程) ⑶ 每一个进程至少包含一个线程,也就是我们的主线程(像 java 中的 main 方法就是来启动主线程的)在主线程中可以创建并启动其他线程。 ⑷ 一个进程的线程共享该进程的内存资源。
2. 创建线程
①通过继承 Thread 来创建线程
- 写一个类继承 java.lang.Thread
- 重写 run(
- 线程中要执行的任务都要写在 run() 中,或在 run() 中进行调用。
public class Demo1 extends Thread{//继承 Thread 类
@Override
public void run() {//重写 run 方法
for (int i = 1; i <= 200; i++) {
System.out.println("run"+i);
}
}
}
public static void main(String[] args) {
//创建线程
Demo1 demo1 = new Demo1();
//启动线程
demo1.start();
for (int i = 1; i <= 200; i++) {
System.out.println("main"+i);
}
}
启动线程调用的是 start() ; 不是 run() run() 这不是启动线程,只是一个方法调用,没有启动线程,还是单线程模式的。
②通过实现 Runnable 接口来创建线程
- 创建任务,只先创建线程要执行的任务,创建一个类,实现 Runnable 接口。
- 重写任务执行的 Run()
- 创建线程,并为线程指定执行任务。
public class Demo2 implements Runnable {//实现 Runnable 接口
@Override
public void run() {//重写 run 方法
for (int i = 0; i < 200; i++) {
System.out.println("自定义线程");
}
}
}
public static void main(String[] args) {
//创建任务
Demo2 demo2 = new Demo2();
//创建线程,并指定执行任务
Thread thread = new Thread(demo2);
thread.start();
}
实现 Runnable 接口创建的优点:
- 因为 java 是单继承,一旦继承一个类就不能在继承其他类,避免单继承的局限。
- 适合多线程来处理同一份资源时使用
3. Thread 类中的方法
run() 用来定义线程要执行的任务代码。 start() 启动线程 currentThread() 获取到当前线程 (.得到具体信息) setName() 为线程设置名字 getState() 获取状态 getPriority() setPriority 获取/设置优先级 sleep() 让当前线程休眠指定时间。 join() 等待当前线程执行完毕,其他线程再执行。 yield() 主动礼让,退出 cpu 重新回到等待序列。
关于优先级: 【java 中默认优先级为 5, 设置优先级范围为 1~10】(作用:为操作系统调度算法提供的)
4. 线程生命周期
线程状态: 新建:刚刚创建了一个线程对象,并没有启动 就绪:调用 start() 后线程就进入到了就绪状态(可运行状态),进入到了操作系统的调度队列 运行状态:获得了 cpu 执行权,进入到 cpu 执行 阻塞状态:例如调用 sleep() ,有线程调用了 join(),线程中进行 Scanner 输入... 死亡/销毁:run() 方法中的任务执行完毕了
状态关系图:

5. 多线程的概念
顾名思义指:在一个程序中可以创建多个线程执行。 【优点】提高程序执行效率(多个任务可以在不同的线程中同时执行) 提高了 cpu 的利用率 改善程序结构,将复杂任务拆分成若干个小任务 【缺点】线程也是程序,线程越多占用内存也越多,cpu 开销变大(扩充内存或升级 cpu) 线程之间同时对共享资源的访问会相互影响,若不加以控制会导致数据出错。
那么如何解决多线程操作共享数据的问题?
6. 线程同步与锁(Lock)
多个线程同时访问操作同一个共享的数据 (例如买票、抢购等) 时,可能会引起冲突,所以引入线程'同步'机制,即各线程间要有先来后到。 即通过【排队 + 锁】在关键的步骤处,多个线程只能一个一个的执行。
synchronized(同步锁) 同步锁对象作用:用来记录有没有线程进入到同步代码块,如果有线程进入同步代码块,那么其他线程就不能进入同步代码块,直到上一个线程执行完同步代码块的内容,释放锁之后,其他线程才能进入。 同步锁对象要求:同步锁对象必须是唯一的。 synchronized(同步锁对象){ 同步代码块 }
synchronized 修饰方法时,同步锁对象不需要我们指定,同步锁对象会默认提供:
- 非静态方法 ------ 默认是 this
- 静态方法 ------ 锁对象是当前类的 class 对象 (一个类的对象只有一个)
ReentrantLock 与 synchronized 区别?
-
synchronized 是一个关键字,控制依靠底层编译后的指令去实现。
-
synchronized 可以修饰一个方法或一个代码块。
-
synchronized 是隐式的加锁和释放锁,一旦方法或代码块出现异常,会自动释放锁。
-
ReentrantLock 是一个类,依靠 java 底层代码去控制 (底层有一个同步队列)
-
ReentrantLock 只能修饰代码块。
-
ReentrantLock 需要手动 的加锁和释放锁,所以释放锁最好写在 finally 中,一旦出现异常,保证锁能释放。
误区:不是只要有线程就需要加锁,只有多个线程对同一资源共享时才加锁
模拟卖票 两个窗口分别售票,票数为 10 张
public class MyThread extends Thread{//我们使用了继承 Thread 的方法
static int num =10; //票总数 10,且为共享资源,要用 static 修饰
static String obj = new String();//可以是任意类对象,但必须唯一。
/* synchronized(同步锁对象) { 同步代码块 } */
@Override
public void run() {//线程要执行的代码块要写在 run() 中
while (true){
synchronized (obj){//加锁,一次只能执行一个线程
if(num>0){
try {
Thread.sleep(800);//此处加入休眠为了让运行结果更明显,也可不加
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了第"+num+"张票");
num--; //每抢一张票,总数 num(10) 就减 1
}else{
break;
}
}
}
}
}
在 Main 方法中创建线程并启动:
public static void main(String[] args) {
//创建两个线程,分别对应两个窗口
MyThread myThread1 = new MyThread();
myThread1.setName("窗口 1");//线程 1
myThread1.start();
MyThread myThread2 = new MyThread();
myThread2.setName("窗口 2");//线程 2
myThread2.start();
}
运行结果:



