目录
StringBuilder、StringBuffer、StringJoiner
LocalDate、LocalTime、LocalDateTime
易混淆
-
长度问题
- 字符串: xx.length();
- 数组: xx.length;
- 集合: xx.size();
-
变量定义
char c = '';//编译错误
String s = ""; // 编译正确
char定义不能为空, String可以
-
死循环
while(){} //编译错误
while(true){} // 正确
for(;;){} //正确
while的判断不能为空, for中可以没有判断
-
关键字
main不属于关键字
基础部分
跨平台
一次编译,处处可用,针对不同的操作系统提供不同的JVM(java虚拟机,运行java)
JDK(Java开发工具包)的组成
- JRE:Java的运行环境(包括JVM和核心类库)
- JVM:Java虚拟机,真正运行Java程序的地方
- 核心类库:Java自己写好的程序,给程序员自己的程序调用的
- 开发工具:Java(运行), Javac(编译)...
关键字
java语言中有特定含义的单词,我们不能随便使用。
标识符
-
使用要求
- 只能由数字、字母、下划线_、$组成
- 数字不能开头,且不能用关键词、区分大小写
-
使用规范
- 变量名:首字母小写、驼峰命名、见名知意;
- 类名:首字母大写、驼峰命名、见名知意
存储单元
-
基本存储单元
Byte(字节) 1Byte = 8 bit
-
最小存储单元
bit(比特、位)
数据类型
-
基本数据类型(四类八种)
- 整型:byte、short、int、long
- 浮点型:float、double
- 字符型:char
- 布尔型:boolean
注:在进行运算时(byte、short、char) 会自动转为int再参与运算
ASCII码(控制字符的编号)常用:
'0':48
'A':65
'a':97
-
引用数据类型
赋值运算符
+=、-=、*=......他们自带强制类型转换。
a += 1; //a = (a的类型)(a + 1)
流程控制
- 顺序
- 分支
- if:适合区间,功能强大
- switch:适合单值,性能较好
注:switch的变量不能是float、double、long。
根据表达式的值直接找到对应的case,执行里面的代码,遇到break则退出switch
- 循环:for、while、do while
随机数
Random r = new Random();
int num = r.nextInt(10); //随机0-10
//公式:r.nextInt(大数 - 小数 + 1) + 小数;
r.nextInt(91) + 10; //10-100
数组
数组是一个用来存储多个“同类型数据”的容器,为引用数据类型
-
静态初始化
//数据类型[] 数组变量名= new 数据类型[]{元素1,元素2...};
int[] arr = new int[]{98, 67, 45};
//简写格式:数据类型[] 数组变量名 = {元素1, 元素2...};
int[] arr = {98,67,45};
-
动态初始化
//数据类型[] 数组变量名 = new 数据类型[数组长度];
int[] arr = new int[30];
-
执行原理(方法区、堆、栈)
jvm(java虚拟机)内存分配 :
- 方法区:程序涉及的类的字节码对象都先加载到方法区
- 栈:运行时main方法及其中的变量都是存在栈中
- 堆:new的对象都是放在堆中的
数组在计算机中的执行原理:
- 先在栈中声明变量 arr
- 再在堆中开辟3个连续int空间,存放数据值
- 将堆中首个元素的地址值赋值给栈中arr,从而arr有了引用
数组置空:
就是将栈中arr的引用地址置为空,不再指向堆内存具体的地址.
int[] arr = null;
注:再访问arr中任何索引或者length都会出现空指针异常
方法
方法就是一些代码打包起来,然后在需要的地方可以重复使用
-
好处
提供代码的复用性和可维护性
-
调用
- 赋值调用:返回值类型 变量 = 方法名(实参列表);
- 直接调用:方法名(实参列表);
- 打印调用:System.out.println(方法名(实参列表));
-
方法重载
规则:多个方法,方法名相同,形参列表不同(数量、类型、顺序)
执行原理:方法的压栈(入栈)、弹栈(出栈)。先入栈的后出栈
注:与返回值无关 ,与形参的变量名无关
好处:方便调用者,不用记很多的名字
面向对象
所有的事物都可以抽象为一个类,用来表述一类事物的属性和行为。而对象是将类实例化成一个具体的事物。在java中,我们把new对象的过程叫做实例化,如:
Student s1 = new Student();
-
与面向过程编程区别
面向过程编程:开发一个一个的方法,有数据要处理了,我们就调方法来处理。
面向对象编程:开发一个一个的对象来处理数据,把数据交给对象,再调用对象的方法来完成对数据的处理。(谁的事情谁来做,谁的数据谁负责)
-
执行原理
如下图,当执行了上面的代码,会在堆中开辟空间,给其成员变量赋默认值;且在栈中开辟空间,存储new出来的对象内存地址。
注:当堆内存中的对象,没有被任何变量引用(指向)时,就会被判定为内存中的“垃圾”。Java存在自动垃圾回收机制,会自动清除掉垃圾对象,程序员不用操心。
-
注意事项
- 类名建议用英文单词,首字母大写,满足驼峰模式,且要有意义,比如:Student、Car…
- 类中定义的变量也称为成员变量(属性),类中定义的方法也称为成员方法(行为)。
- 成员变量本身存在默认值,在定义成员变量时一般来说不需要赋初始值(它只是一类事物的抽象,赋值没有意义)。
- 对象名要求和类名相似,但是需要首字母小写,建议和类名同名。
- 一个代码文件中,可以写多个class类,但只能一个用public修饰,且public修饰的类名必须成为代码文件名。
-
**封装**
将事物的属性和行为抽象到类中,且其中的数据不能被随意访问。
原因: 在类中默认定义的变量是随时可被其他类访问、修改,数据是不安全的。
如何封装: 将成员变量用private修饰,进行私有化,只能本类进行访问。对外提供公共的getter/setter方法,增加访问数据的条件。
-
**继承 (extends)**
extends关键字,如下B继承于A,建立起父子关系。子类会继承父类所有非私有成员
public class B extends A{
}
java只支持单继承,避免出现几个父类拥有同名方法,继承时产生冲突。但支持多层继承,也就是其父类也可以有父类,所有类的最终父类为Object类
执行原理
如图,extends会将A与B在方法区中建立连接,且在堆中开辟对象空间时,其继承的属性也会存储进来。其目的是减少重复代码的编写。
方法重写
当子类觉得继承的方法无法满足需求时,可进行方法的重写:
子类写一个方法名称、参数列表、返回值类型一致的方法,覆盖掉继承的方法,这就是方法的重写。 @Override注解可以让编译器检查重写是否正确。
注:
- 子类重写父类方法时,访问权限必须大于或者等于父类该方法的权限
- 重写的方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小(若原返回值类型是类,则重写后可以是其子类)。
- 私有方法、静态方法不能被重写,如果重写会报错的。
权限修饰符
用来修饰类中的成员,以限制被修饰成员能够被访问的范围。
修饰符 | 本类 | 本包 | 子孙类 | 任意类 |
private | √ | |||
缺省(默认) | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
super关键字
super.父类成员变量/方法
用于指定访问父类的成员,一般当父类与子类出现重名成员时会用到。与this类似,this是成员变量与方法内部变量重名时,用于调用成员变量。其原因是访问成员遵循就近原则。
子类构造器默认在第一行有 super() ,用于调用父类无参构造。super()并不会创建新的父类对象,而是在初始化子类对象的过程中,调用父类的构造方法来完成对父类部分的初始化工作。
注:如果父类没有无参构造,则必须在子类构造器的第一行手写super(…),指定去调用父类的有参构造器。
this(...) 用于调用该类的其他构造器,一般在构造方法中用该关键字调用其他构造方法,来传递默认值。
两者都必须放在构造器第一行,因此不能同时存在
-
**多态**
父类的引用指向子类的对象,可以是不同子类。也就是父类指向不同对象的多种状态,即对象多态。当指向不同的子类时,其重写的方法会有不同的行为,也就是父类不同行为的多种状态,即行为多态。
继承和实现都可用多态。属性不谈多态
People p1 = new Student(); //对象多态
p1.run(); //行为多态
People p2 = new Teacher();
p2.run();
多态本质上相当于用父类的模型盒子到子类中去截取。以达到在不改变父类的情况下,实现不同子类的个性化行为。
前提
- 有继承/实现关系
- 存在父类引用指向子类对象
- 存在方法重写
好处
- 等号左右两边松耦合,更便于修改和维护。只需用修改等号右边
- 定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利
弊端
多态下不能使用子类的独有功能,想要调用需进行强制类型转换。
类型转换
- 自动类型转换:父类 变量名 = new 子类();
- 强制类型转换:子类 变量名 = (子类) 父类变量;
注:强制类型转换时,对象的真实类型与强转后的类型不一致,运行时会报错(ClassCastException)。可使用 instenceof 判断数据类型。
-
静态 (static)
修饰成员变量和方法,用于共享数据。
类加载时,数据随之加载,不用等到创建对象,且只加载一次,该类的所有对象都将共享这同一份数据。
- 静态变量 (类变量):共享属性和同一块内存地址,意思是所有对象的该属性都指向同一块内存地址,一个改变都改变。访问:类名.类变量 、对象名.类变量
- 静态方法 (类方法):一般用作工具类,不用创建对象,直接调用,方便且可节省下创建对象的内存。访问:类名.类方法、对象名.类方法
- 静态代码块:static {} 用于对类变量的初始化赋值。可以先定义类变量,然后在其中对类变量统一进行初始化。与之相对应的实例代码块:{} 在创建对象时,构造方法前执行,用于实例变量初始化
执行原理
如下图,当在方法区加载Student类时,会直接在堆内存中开辟静态属性name的空间。创建对象时,会在栈中开辟存放地址的变量,指向堆中开辟的对象空间。所有对象操作同一个静态属性name。其实本质上静态变量和静态方法都是存储在方法区(元空间)中的。
-
final
- 修饰类:该类不能再被继承
- 修饰方法:该方法不能被重写
- 修饰变量:该变量只能被赋值一次,赋值完毕之后不能再修改
注:如果 final 修饰的是引用类型的变量,变量所存储的地址不能被改变,但是地址所指向对象的内容是可以被改变的
-
抽象类 (abstract)
用 abstract 关键字修饰类、成员方法。将多个类中同一方法但不同行为抽象出来,成为抽象方法,不写方法体。抽象类中可以没有抽象方法,但有抽象方法的类必须是抽象类。 一般用于继承
其本质上就是将父类中可能被大量重写的方法的方法体去掉,然后加上关键字。
public abstract class A {
// 抽象方法:必须abstract修饰,只有方法签名,不能有方法体。
public abstract void test();
}
主要特点
- 抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
- 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
模板方法设计模式
一个功能的完成需要经过一系列步骤,这些步骤是固定的,但是中间某些步骤具体行为是待定的,在不同的场景中行为不同。在方法中不同的部分调用抽象方法,来让子类具体实现。子类只需要继承该抽象类,重写抽象方法即可完成功能
因模板方法是固定的,不能被子类重写,因此一般使用final关健字修饰模板方法
abstract class Person {
//模板方法
public final void work(){
System.out.println("吃饭");
action();
System.out.println("睡觉");
}
public abstract void action();
}
-
接口 (interface)
关键字interface
接口可以多实现(一个类实现多个接口),多继承(一个接口是可以继承多个接口)。
在继承中因为怕继承下来的方法冲突,而不支持多继承。如果父类所有的方法都为抽象方法,也就是说没有方法体,那就不会产生冲突。这样完全抽象的类我们可以把它认为是接口并用interface修饰。
接口本质上是一种完全抽象,可以多实现多继承的抽象类,其可以看作是一种规范或者契约
public interface 接口名 {
int age = 18; //接口中的成员变量都是常量, 默认是被public static final修饰的
void action(); //接口中的成员方法都是抽象方法, 默认是被public abstract修饰的)
//注意: 接口中不能有构造方法和代码块
}
实现类 (implements)
实现接口的类被称为实现类。(implements)一个类可以实现多个接口
public class 实现类 implements 接口1, 接口2, 接口3 , ... { }
必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类。
好处
可以面向接口编程,因其多继承多实现的特性,使各种业务实现灵活方便。(解耦合)
JDK8 后新增形式
- 默认方法:使用default修饰,使用实现类的对象调用。
- 静态方法:static修饰,必须用当前接口名调用。
- 私有方法:private修饰,jdk9开始才有的,只能在接口内部被调用。
他们都会默认被public修饰。
public interface A{ /** * 1、默认方法(jdk8开始支持):对接口中的方法提供默认实现 * 使用default修饰,有方法体,可以但是不强制要求实现类重写, * 只能通过实现类的对象调用*/ default void test1(){...} /** * 2、静态方法(jdk8开始支持):方便调用 * 使用static修饰,有方法体,只能通过接口名调用*/ static void test2(){...} /** * 3、私有方法(jdk9开始支持):提高代码复用性 * 使用private修饰,服务于接口内部,用于抽取相同的功能代码*/ private void test3(){...} }
-
常量 (static final)
使用了 static final 修饰的成员变量就被称为常量。可直接用 类名.常量名 调用。
优势:
代码可读性更好,提高可维护性
public class Constant {
public static final String SCHOOL_NAME = "清华";
}
注:常量名的命名规范:建议使用大写英文单词,多个单词使用下划线连接起来。
枚举(enum)
枚举是一种特殊类,也是创建常量的一种形式。将要用到的标识符放到第一行,用逗号隔开。且调用的值就是本身,本身就是字符串类型
//创建枚举 enum SexEnum { MAN,WOMEN; } //实体类 class Student { private String name; private SexEnum sex; ... } public static void main(String[] args) { Student student = new Student(); student.setSex(SexEnum.MAN); //将"MAN"传给对象 }
注意事项:
- 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
- 枚举类的构造器都是私有的(写不写都只能是私有的),因此,枚举类对外不能创建对象。
- 枚举都是最终类,不可以被继承。
- 枚举类中,从第二行开始,可以定义类的其他各种成员。
- 编译器为枚举类新增了几个方法,并且枚举类都是继承:java.lang.Enum类的,从enum类也会继承到一些方法。
-
this关键字
this关键字一般是在类的方法中出现,当生成的对象调用该方法时,栈中就会生成一个this变量空间,其中就会存储该对象的地址,来指向堆中的对象 。
作用:其是用来解决当对象的成员变量与方法内部变量的名称一样时,所导致的访问冲突问题。
因为成员变量和局部变量在类中是可以同名的,同名时在方法中调用的优先级是就近原则
-
成员变量与局部变量区别
-
构造器
又叫构造方法,用于创建对象和给对象赋值。
一个类中,如果没有任何的构造方法,Java会自动生成一个无参构造。
一个类中,一旦有了有参构造方法,Java将不会再生成无参构造,此时如果需要无参构造,需要自行创建。
-
JavaBean
又叫实体类,就是一种特殊形式的类。
其只负责数据的存取,而对数据的处理交给其他类来完成,以实现数据和数据业务处理相分离。
-
内部类
类中的五大成分之一(成员变量、方法、构造器、代码块、内部类)
- 成员内部类:与成员属性平级
public class Outer { // 成员内部类 public class Inner { // 成员属性和方法 } } //创建成员内部类对象 Outer.Inner in = new Outer().new Inner();
- 静态内部类:使用static修饰的成员内部类
public class Outer{ // 静态内部类 public static class Inner{ } } //创建静态内部类对象 Outer.Inner in = new Outer.Inner();
- 局部内部类:定义在方法中、代码块中、构造器等执行体中
- 匿名内部类:不需要为这个类声明名字,本质就是一个子类,并会立即创建出一个子类对象,通常作为一个参数传输给方法。
public static void main(String[] args) { //调用go方法,参数需要Swimming对象 go(new Swimming() { @Override public void swim() { System.out.println("猫也还行~~~"); } }); } public static void go(Swimming s){ System.out.println("开始============"); s.swim(); } public interface Swimming{ void swim(); }
-
泛型
泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!这样可以避免强制类型转换,及其可能出现的异常。
把具体的数据类型作为参数传给类型变量。
注:
- 类型变量建议用大写的英文字母,常用的有:E、T、K、V 等
- 泛型是工作在编译阶段的,一旦程序编译成class文件,class文件中就不存在泛型了,这就是泛型擦除。
- 泛型不支持基本数据类型,只能支持对象类型(引用数据类型)。
泛型类
修饰符 class 类名<类型变量,类型变量,…> { } public class ArrayList<E>{ . . . }
泛型接口
修饰符 interface 接口名<类型变量,类型变量,…> { } public interface A<E>{ . . . }
泛型方法
修饰符 <类型变量,类型变量,…> 返回值类型 方法名(形参列表) { } public static <T> void test(T t){ }
泛型的上下限
- 泛型上限:? extends Car: ? 能接收的必须是Car或者其子类 。
- 泛型下限:? super Car : ? 能接收的必须是Car或者其父类。
包
包是用来分门别类的管理各种不同程序的,类似于文件夹,建包有利于程序的管理和维护。
注意事项:
- 同一个包下的类,互相可以直接调用。
- 访问其他包下某个类需要导包。
import 包名.类名
- Java.lang包下的程序是不需要我们导包的,可以直接使用。
- 如果当前类中,要调用多个不同包下的类,而这些类名正好一样,此时默认只能导入一个类,另一个类必须带包名访问。
String
字符串对象创建方式:
- String name = "字符串数据";
- String name = new String("字符串数据");
- String name = new String(字符数组)
-
常用API
-
注意事项
- 只要是以“...”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份。
- 通过new方式创建字符串对象,每new一次还会产生一个新的对象放在堆内存中。
-
执行原理
String s1 = "abc";
String s2 = "abc";
String s1 = new String("abc");
String s2 = new String("abc");
注:如果字符串拼接,编译时java会直接拼接到字符串常量池中,如下代码1中,s1与s2中所存储的地址值是一致的。而如果是变量参与拼接,是在堆中new出对象空间,如下代码2中,s1与s2中所存储的地址值是不一致的。
//代码1
String s1 = "abc" + "def";
String s2 = "abcdef";
//代码2
String s1 = "abc";
s1 += "def";//abcdef
String s2 = "abcdef";//abcdef
-
不可变性
每次试图改变字符串对象,实际上是产生了新的字符串对象,变量每次都是指向了新的字符串对象,之前字符串对象的内容确实是没有改变的,因此说String的对象是不可变的。
集合(ArrayList<>)
一种容器,用来装数据的, 类似于数组, 但是长度是可变的
ArrayList<数据类型> 集合名称 = new ArrayList<>();
注:<数据类型>是添加了泛型,通过泛型可以约束集合中存放的数据的类型
-
特点
有序,数据可重复
-
常用API
-
删除数据注意事项
索引倒着删除 快捷键:forr
索引正着删除,注意删除一个将索引--,因为每删除一个数据,下面的数据会自动上移,此时索引会指向原来的下一个数据,因此要索引--,才能让++后指向这个数据。
进阶部分
常用API
-
Object
Object类是所有类的父类,其中提供了所有类都可能用到的方法,供子类去重写。
- toString() 方法存在的意义就是为了被子类重写,以便返回对象具体的内容。
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }
- equals() 存在的意义就是为了被子类重写,以便子类自己来定制比较规则(比如比较对象内容)。
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); }
注:这两个方法中的方法体都会自动生成
- clone() 这个方法分为浅克隆和深克隆。
- 浅克隆拷贝出的新对象,与原对象中的数据一模一样(引用类型拷贝的只是地址)。
@Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }
- 深克隆是对象中基本类型的数据直接拷贝,对象中的字符串数据拷贝的还是地址,对象中的其他对象,不会拷贝地址,会创建新对象。
@Override protected Object clone() throws CloneNotSupportedException { Student cloneStudent = (Student) super.clone(); cloneStudent.scores = this.scores.clone(); //其中的scores为数组 return cloneStudent; }
-
Objects
是一个工具类,提供了很多操作对象的静态方法。
- equals(Object a, Object b) 先做非空判断,再比较两个对象
- isNull(Object obj) 判断对象是否为null,为null返回true
- nonNull(Object obj) 判断对象是否不为null,不为null则返回true
//1. 定义两个字符串对象 String s1 = null; String s2 = "itheima"; //2. 判断两个对象是否相等 boolean b = Objects.equals(s1, s2);//false //3. 判断对象是否为空 boolean aNull = Objects.isNull(s1);//true //4. 判断对象是否不为空 boolean aNull1 = Objects.nonNull(s1);//false
-
包装类
包装类就是把基本类型的数据包装成对象。
(int -> Integer char -> Character 其他都为首字母大写)
- 自动装箱:基本数据类型可以自动转换为包装类型。
- 自动拆箱:包装类型可以自动转换为基本数据类型。
- valueOf() 将基本数据类型或者字符串转变为包装类
Integer a = Integer.valueOf(7); //可以省略方法让其自动装箱 Integer a1 = 7; Integer b = Integer.valueOf("71");
- toString() 包装类可以把基本数据类型转变字符串类型。
Integer a = 6; //此时的a是包装类对象 String str1 = a.toString(); //调用其中的转字符串方法 int b = 10; //此时的a是基本数据类型 //用Integer类中的静态方法,也就是工具,将其变为字符串 String str2 = Integer.toString(b);
- parseInt() 包装类可以将字符串类型的数值转变为数值本身的基本数据类型,也可用valueOf()将字符串转变为包装类。
String s = "1234"; int i = Integer.parseInt(s);
注:parse意味解析,一般parse都是用来将字符串类型,解析(转变)为需要的类型。除了将字符串解析成基本数据类型之外,后面还有将字符串解析成日期格式。
-
StringBuilder、StringBuffer、StringJoiner
都用来操作字符串,相当于容器,比String编写简单且效率更高
StringBuilder用法
//1. 创建StringBuilder StringBuilder sb = new StringBuilder("abc"); //2. 字符拼接 sb.append("def"); //abcdef //3. 反转内容 sb.reverse(); //fedcba //4. 返回长度 System.out.println(sb.length()); //6 //5. 转为字符串 String s = sb.toString();
注:主要用于字符串的大量拼接和反转,拼接效率远大于String。
StringBuffer
StringBuffer的用法与StringBuilder是一模一样的。与StringBuilder相比这个线程安全, 但性能稍差。
StringJoiner用法
int[] arr = {11, 22, 33}; //设置特定格式的对象,参数分别为间隔符、开始符号、结束符号 StringJoiner sj = new StringJoiner(",","[","]"); for (int i = 0; i < arr.length; i++) { //其会按照特定格式拼接 sj.add(arr[i] + ""); } //获取长度 System.out.println(sj.length()); //转字符串 String str = sj.toString();
注:主要用于需要固定格式拼接的场景
-
Math
工具类,提供对数据进行操作的静态方法。
//绝对值 Math.abs(-5); //5 //向上取整 Math.ceil(4.1); //5.0 //向下取整 Math.floor(5.9); //5.0 //四舍五入 Math.round(5.1); //5 //取最大值 Math.max(4, 5); //5 //取最小值 Math.min(5, 6); //5 //取5的1次幂 Math.pow(5, 1); //5 //返回值为double的随机值,范围[0.0,1.0) Math.random();
-
System
工具类,代表程序所在的系统。
//获取当前系统时间的毫秒值 long a = System.currentTimeMillis(); //终止当前运行的Java虚拟机, 非0参数表示异常停止 System.exit(0);
注:毫秒值指的是从1970年1月1日 00:00:00走到此刻的总的毫秒数,东八区为8点。
-
Runtime
代表程序所在的运行环境。
//获取Runtime对象 Runtime runtime = Runtime.getRuntime(); //java虚拟机可用的处理器数 int processors = runtime.availableProcessors(); //java虚拟机中总内存 单位B long totalMemory = runtime.totalMemory(); //Java虚拟机中的可用内存 long freeMemory = runtime.freeMemory(); //启动某个程序,并返回代表程序的对象 Process process = runtime.exec("C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"); process.destroy();//关闭程序
-
BigDecimal(精确小数运算)
用于解决浮点型运算时,出现结果失真的问题。一般在有金额运算的时候会用到。
//把String转成BigDecimal BigDecimal bi1 = new BigDecimal("10"); //将 double转换为BigDecimal BigDecimal a = new BigDecimal(10.0); //不推荐使用,无法总精确运算 //将 double转换为BigDecimal BigDecimal bi2 = BigDecimal.valueOf(3); //加 BigDecimal add = bi1.add(bi2); //减 BigDecimal subtract = bi1.subtract(bi2); //乘 BigDecimal multiply = bi1.multiply(bi2); //除 BigDecimal divide1 = bi1.divide(bi2, 2, RoundingMode.UP);//进一 2位小数 BigDecimal divide2 = bi1.divide(bi2, 2, RoundingMode.FLOOR);//去尾 BigDecimal divide3 = bi1.divide(bi2, 2, RoundingMode.HALF_UP);//四舍五入 //将BigDecimal转换为double double doubleValue = bi2.doubleValue();
注:调用加减乘除方法之后需要接收,且原数对象内容不会改变
-
JDK8之前的日期、时间
Date
//创建时间对象,无参是当前时间 Date date = new Date(); //获取当前时间毫秒值 long timeMillis = System.currentTimeMillis(); //设置日期对象的时间为参数毫秒值对应的时间 Date date1 = new Date(timeMillis - 3600 * 1000); //获取指定date对应的毫秒数 long time = date.getTime(); //设置日期对象的毫秒值,用于修改日期对象 date.setTime(timeMillis - 3600L *24*1000*30*12);
注:获得日期对象,需要往参数中放毫秒值,或者无参获取当前日期
SimpleDateFormat
对日期进行格式化,可将日期变成想要的形式
//创建简单日期格式化对象, 参数为想要的格式 SimpleDateFormat form = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss"); Date date = new Date(); //将当前时间对象格式化成目标格式的字符串 String s = form.format(date); //把字符串时间解析成日期对象 Date date1 = form.parse(s);
注:创建格式化对象,主要用于把日期格式化成字符串,或者解析字符串为日期对象。想要解析字符串,就必须要字符串对应的格式;而所有格式的日期对象,都可通过format方法格式化成想要的格式的字符串。
Calendar
这是独立于Date的。代表的是系统此刻时间对应的日历,通过它可以单独获取、修改时间中的年、月、日、时、分、秒等;使操作简化。
// 创建日历对象 将当前时间的所有信息变量都放入对象中 Calendar calendar = Calendar.getInstance(); // 获取日历中指定信息 年\月\日 int year = calendar.get(Calendar.YEAR); int month = calendar.get(Calendar.MONTH); int day = calendar.get(Calendar.DAY_OF_MONTH); // 获取时间对象,变成Date可用格式化操作 Date time = calendar.getTime(); // 获取时间毫秒值 long timeInMillis = calendar.getTimeInMillis(); // 修改日期中的某个项 calendar.set(Calendar.YEAR,2021); // 为某个信息增加/减少指定的值 calendar.add(Calendar.MONTH,12);
注:calendar是可变对象,一旦修改后其对象本身表示的时间将产生变化。且只能对此刻时间进行操作。
-
JDK8之后的日期、时间
LocalDate、LocalTime、LocalDateTime
- LocalDate:年、月、日
- LocalTime:时、分、秒
- LocalDateTime:年、月、日、 时、分、秒
LocalDateTime中的方法是前两者的和。
// 两种方式创建LocalDateTime LocalDateTime now = LocalDateTime.now(); //当前时间 LocalDateTime of = LocalDateTime.of(2000, 6, 6, 6, 6); //设置的时间 // 使用LocalDateTime获取LocalDate和LocalTime LocalDate localDate = now.toLocalDate(); //转换成一个LocalDate对象 LocalTime localTime = now.toLocalTime(); //转换成一个LocalTime对象 // 获取年月日等 int year = now.getYear(); //年 int month = now.getMonthValue(); //月 int day = now.getDayOfMonth(); //日 int hour = now.getHour(); //时 int minute = now.getMinute(); //分 int second = now.getSecond(); //秒 // 直接修改某个信息,返回新日期对象: // withYear、withMonth、withDayOfMonth、withDayOfYear LocalDateTime newTime = now.withYear(2023); //修改年份为2023 // 把某个信息加多少,返回新日期对象: // plusYears、plusMonths、plusDays、plusWeeks LocalDateTime lt1 = now.plusYears(10).plusMonths(1); // 把某个信息减多少,返回新日期对象: // minusYears、minusMonths、minusDays,minusWeeks LocalDateTime lt2 = now.minusYears(10); //减少10年 // 判断两个日期对象,是否相等,在前还是在后 now.equals(of); //false 不相等 now.isBefore(of); //false now在后,of在前 now.isAfter(of); //true
注:这个相对于之前的Date来说:Date只能通过毫秒值进行操作设置,操作麻烦,其中的Calendar又只能以当前时间为基准操作。LocalDateTime能直接设置时间并操作,且为不可变对象,操作后原时间对象不会改变丢失。
ZoneId、ZonedDateTime
- ZoneId:时区
- ZonedDateTime:带时区的时间
//获取所有的时区 Set<String> zoneIds = ZoneId.getAvailableZoneIds(); //获取系统默认时区 ZoneId zoneId = ZoneId.systemDefault(); //指定一个时区对象 ZoneId zoneId1 = ZoneId.of("Asia/Tokyo"); //获取指定时区的ZonedDateTime对象 ZonedDateTime now = ZonedDateTime.now(zoneId1);
注:获取年月日等、操作修改方法与LocalDateTime的方法一致
DateTimeFormatter
用于时间的格式化和解析。
//初始化DateTimeFormatter对象 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); LocalDateTime now = LocalDateTime.now();//当前时间 //当前时间进行格式化 String str = now.format(dtf); //将时间字符串,解析为LocalDateTime对象 String time = "2024-06-15 15:57:39"; LocalDateTime dateTime = LocalDateTime.parse(time, dtf);
注:在Date中,格式化和解析方法在格式化对象里;而在LocalDateTime中,方法就在日期对象里,格式化对象只能作为一个参数。
-
Arrays
操作数组的一个工具类。
常用方法
int[] arr = {1,3,2,5,4}; // 返回数组内容的字符串表示形式 String arrStr = Arrays.toString(arr); // 拷贝数组,元素为索引的指定范围,左包含\右不包含,超过范围赋值0 int[] range = Arrays.copyOfRange(arr, 1, 8); // 拷贝数组,指定新数组的长度,多余的赋值0 int[] copyOf = Arrays.copyOf(arr, 10); //将数组中的数据改为新数据,重新存入 double[] arrDouble = {3,4,5,6,7}; Arrays.setAll(arrDouble, new IntToDoubleFunction() { @Override public double applyAsDouble(int index) { return arrDouble[index] * 10; } }); // 对数组元素进行升序排列(存储基本数据类型) Arrays.sort(arr);
对存储自定义对象的数组进行排序
排序规则:底层用当前元素与已有元素循环进行比较。返回正数,表示当前元素较大;返回负数,表示当前元素较小;返回0,表示相同。
- 自然排序
实现Comparable接口,指定泛型 重写compareTo方法 指定排序规则- 比较器排序
调用sort排序方法,参数二传递Comparator按口的实现类对象(匿名内部类实现) 重写compare方法 指定排序规则//自然排序 Arrays.sort(students); class Student implements Comparable<Student>{ private int age; @Override public int compareTo(Student o) { return this.age - o.age; } }
//比较器排序 Arrays.sort(teachers, new Comparator<Teacher>() { @Override public int compare(Teacher o1, Teacher o2) { return o1.getAge() - o2.getAge(); } });
异常
异常体系中分为 Error 和 Exception,其中Error为系统级别错误,因此通常将Exception叫异常。分为运行时异常和编译时异常。如果不处理异常,程序会直接中断。
- 运行时异常:RuntimeException及其子类,只有运行时才会出现
- 编译时异常:编译阶段就会出现,没有继承RuntimeException的异常
如果是运行时异常,会将异常默认抛给调用者,一直向上传递,直到被处理;
如果是编译时异常,需要手动在方法上添加 throws关键字 声明抛出。如下:
方法 throws Exception{ }
处理异常
直接在可能出现异常的代码上添加 try--catch 结构 捕获并处理异常,异常处理后,后续的代码是可以继续执行的。
try{ // 可能出现异常的代码! }catch (Exception e){ // 在此处处理异常 }
自定义异常
异常最主要的运用是自定义异常,也分为运行时异常和编译时异常。已有的异常并不能囊括所有业务的需求。比如年龄的接收,负数以及大于200都为不正常的值。可以将这些自定义为异常类,以便用异常统一管理问题。
在方法中可能有错误的地方使用 throw 关键字来抛出异常。自定义编译时异常继承Exception类,运行时异常继承RuntimeException类。
//在需要的业务逻辑处抛出 throw new LoginException("xxx"); //编译时异常: class LoginException extends Exception { public LoginException() { } public LoginException(String message) { super(message); } }
Lambda表达式
简化匿名内部类的写法,且只能简化函数式接口的匿名内部类
函数式接口:只有一个抽象方法的接口,一般函数式接口上面都会加@FunctionalInterface 的注解,其只有标记作用,有该注解的接口就必定是函数式接口。
表达式写法如下:
//实现函数式接口的匿名内部类 new 类或接口(参数值…) { @Override 方法实现(被重写方法的形参列表){ 被重写方法的方法体代码 } }; //Lambda (被重写方法的形参列表) -> { 被重写方法的方法体代码。 }
省略写法:
- 参数类型可以省略不写
- 如果只有一个参数,小括号() 也可以省略
- 如果Lambda表达式中的方法体代码只有一行代码,可以省略 花括号{} 不写,同时要省略 分号 ,如果这行代码是return语句,也必须去掉 return
方法引用
- 静态方法引用:类名::静态方法 Lambda表达式中只调用了一个静态方法,且重写的方法参数与调用方法参数一致
- 实例方法引用:对象名::实例方法
- 特定类型方法引用:参数1类型::方法 表达式中为:参数1.方法(参数2, 参数...)
- 构造器引用:类名::new
正则表达式
一般用途(操作字符串):
- 校验(matches)
String qq = "12345678" qq.matches("[1-9]\\d{5,19}");//不以0开头,长度6-20
- 替换(replaceAll)
String s1 = "古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴"; s1.replaceAll("\\w+", "-");//非中文字符替换成-
- 分割(split)
String s3 = "古力娜扎ai8888迪丽热巴999aa5566马尔扎哈fbbfsfs42425卡尔扎巴"; String[] names = s3.split("\\w+"); // 获取人名
- 爬取:先将字符串预编译成 Pattern 对象,也就是一种模式,这步会解析正则表达式,以便之后复用。然后调用其中的 matcher() 方法,创建 Matcher 实例,其中有将要搜寻的原字符串和正则规则,也就是所谓的匹配器。最后根据所需调用匹配器中的方法。
// 定义规则,正则表达式 String regex = ""; // 把正则表达式封装成一个Pattern对象 Pattern pattern = Pattern.compile(regex); // 通过pattern对象去获取查找内容的匹配器对象。 Matcher matcher = pattern.matcher(data); // 定义一个循环开始爬取信息 while (matcher.find()){ String rs = matcher.group(); // 获取到了找到的内容了。 System.out.println(rs); }
集合(Collection)
一种容器,用来装数据的, 类似于数组, 但是长度是可变的。集合主要分为 单列集合(Collection)和 双列集合(Map)
Collection集合体系
Collection集合特点
集合中只能存储引用数据类型,基本数据类型会被Java自动拆箱和装箱。
List:有序可重复
ArrayList、LinkedList
Set:不重复
HashSet:无序
LinkedHashSet:存取有序
TreeSet:可排序,默认升序
Collection的常见方法
Collection是单列集合的祖宗,它的方法(功能)是全部单列集合都会继承的。
注意:其中的 toArray() 方法,其只会返回 Object 类型数组,想要指定数组类型,需要在参数中创建数组对象,如下
//Object[] toArray() 将集合中元素存入一个对象数组并返回 Object[] objects = list.toArray(); //T[] toArray(T[]a) 将集合中元素存入一个指定类型的数组并返回(指定数组长度) String[] strs = list.toArray(new String[list.size()]);
Collection的遍历方式
迭代器
可类比指针,但是一种比指针更抽象、更面向对象的概念。
需要注意:使用迭代器遍历集合时,又同时在删除集合中的数据,程序就会出现并发修改异常的错误。这是因为集合的结构可能因元素的删除而发生变化(例如,数组列表中的元素下标需要调整),而迭代器对此并不知情,继续按照原来的索引或预期进行,可能导致ConcurrentModificationException异常抛出,或者更糟糕的是,导致某些元素被跳过或重复处理。
用迭代器自己的删除方法删除数据即可
// 获取迭代器对象 Iterator<String> ite = collection.iterator(); // 使用迭代器遍历 while (ite.hasNext()){ // 判断当前位置是否有元素存在 //获取元素 String s = ite.next(); System.out.println(s); }
增强for
增强for遍历集合,本质就是迭代器遍历集合的简化写法,因此也需要注意并发修改异常。在其中修改元素,原集合并不会发生变化。
//2. 使用增强for循环遍历 for (String s : collection) { System.out.println(s); }
Lambda
collection.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }); // Lambda表达式方式遍历集合 collection.forEach(s -> System.out.println(s));
-
List
存取有序,可重复
因 List 集合有索引,所以可以用 普通for 进行遍历
List特有方法
ArrayList
底层基于数组实现,查询快,增删慢
- 原理:
- 利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组。
- 添加第一个元素时,底层会创建一个新的长度为10的数组
- 存满时,会扩容 1.5 倍
- 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准
LinkedList
底层基于双链表实现,查询慢,增删快
(链表中的结点是独立的对象,在内存中是不连续的,每个结点包含数据值和下一个结点的地址,双链表还包含上一个节点的地址)
- 新增方法
- 设计队列
两端开口,先进先出,后加前删
LinkedList<String> queue = new LinkedList<>(); //从队列后端入队列: addLast方法 queue.addLast("第1位顾客"); queue.addLast("第2位顾客"); //从队列前端出队列: removeFirst方法 System.out.println(queue.removeFirst()); System.out.println(queue.removeFirst());
- 设计栈
顶端开口,先进后出,前加前删
LinkedList<String> stack = new LinkedList<>(); //进栈/压栈: push方法(底层封装了addFirst 方法) stack.push("第1颗子弹"); stack.push("第2颗子弹"); //出栈/弹栈: pop方法底(底层封装了removeFirst方法) stack.pop(); stack.pop();
-
Set
不可重复
几乎没有额外新增一些常用功能,常用方法基本上就是Collection提供的。
HashSet
存取无序,底层基于哈希表实现。jdk8之前(数组+链表),8之后(数组+链表+红黑树)
Java中每个对象都有一个哈希值,是int类型数值,Object的hashCode方法根据 对象地址值 计算哈希值,不同对象的哈希值一般不同
- 原理
- 创建一个默认长度16的数组,默认加载因子为0.75,数组名table。也就是存满到16*0.75=12时,就自动扩容,每次扩容原先的两倍
- 使用元素的哈希值对数组的长度求余计算出应存入的位置
- 判断当前位置是否为null,如果是null直接存入
- 如果不为null,表示有元素,则调用重写的equals方法比较
第二步需要用到哈希值,如果元素是内容相同,但不同地址的对象,则计算的哈希值不同,进入的位置就不同,无法达到去重效果,因此需要 重写hashCode方法
jdk8之后,底层加入红黑树,当数组长度 >=64 ,链表长度超过8,自动将链表转为红黑树。
LinkedHashSet
存取有序,底层基于哈希表,原理与 HashSet 一致,但每个元素都额外的多了一个双链表的机制记录它前后元素的位置。
TreeSet
可排序(默认升序排序 ,按照元素的大小,由小到大排序),底层基于红黑树。
自定义类型若为对象,则默认无法排序。需要类 实现Comparable接口,重写里面的compareTo方法;或者在TreeSet集合有参数构造器的参数中设置 匿名内部类Comparator对象。用此返回的 1、0、-1 来设置红黑树的结构,达到排序效果。
可变参数
一种特殊形参,定义在方法、构造器的形参列表里。
定义格式:
方法名(数据类型... 形参名称){ }
其中可以传0或多个数据,也可传入数组。其在方法内部就是一个数组,因为编译后形参会被直接编译为数组格式。
public static void main(String[] args) { int[] arr = {1,2,3}; System.out.println(sum(arr)); } //形参为可变参数 public static int sum(int... nums){ int sum = 0; //作为数组运用 for (int num : nums) { sum += num; } return sum; }
Collections(工具类)
用来操作集合,基本多是对List集合的操作
List<Integer> list = new ArrayList<>(); //static <T> boolean addAll(单列集合,可变参数) 批量添加元素 Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); //static void shuffle(List集合) 打乱List集合元素顺序,每次调用都会打乱 Collections.shuffle(list); //static <T> void sort(List集合) List集合进行自然排序 Collections.sort(list); //比较器对象指定规则排序,参考常用API中 Arrays 的sort方法
Map集合(双列集合)
每个元素为两部分组成,key为键,value为值,一一对应,映射关系。键不允许重复,键若为对象,对象中的元素不能进行修改,因为若是HashMap集合,键底层为哈希表,修改对象,会使哈希值变化,这是由于重写的hashCode方法是根据对象数据获取的,哈希值的改变使得可能求取不到原来的数组位置,以致根据键获取不到值;若为TreeMap,底层为红黑树,键的修改可能导致比较器中数据的改变(如大值变小,本在右下要变到坐下),致使内部结构破坏。
Map集合体系
Map集合特点
注:Map系列集合的特点都是由键决定的,值只是一个附属品,值是不做要求的
HashMap:
无序、不重复
LinkedHashMap:
有序、不重复
TreeMap:
按照大小默认升序排序、不重复
Map常用方法
Map遍历方式
遍历键找值
Set<String> keys = map.keySet(); for (String key : keys) { String value = map.get(key); System.out.println(key + "---" + value); }
键值对
for (Map.Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "---" + value); }
Lambda, 使用foreach
map.forEach((key,value) -> System.out.println(key + "---" + value));
Stream流
用于操作集合或者数组的数据(主要用于查询和转换,不用于修改原始数据源),其步骤主要分为三步:获取、操作、终结。如果修改的是对象的状态,而不是集合的结构或引用关系,那是可以的,但不建议。
获取Stream流
//集合 list.stream() //获取 .forEach(e -> System.out.println(e)); //数组 Arrays.stream(arr) //获取 .forEach(System.out::println); //零散数据 Stream.of("玄奘", "悟空", "悟能", "悟净") //获取 .forEach(System.out::println);
中间操作方法
list.stream() //过滤 .filter(score -> score >= 60) //默认升序 .sorted() //降序 .sorted((o1, o2) -> o2 - o1) //跳过三个 .skip(3) //截取4-6 .limit(3) //对每个元素做相同操作 .map(e -> e + 10) //去重 .distinct() .forEach(System.out::println); //合并两个流为一个流 Stream.concat(list1.stream(), list2.stream()) .forEach(System.out::println);
终结方法
//1. 打印出集合中所有元素 list.stream().forEach(System.out::println); //2. 统计出身高不足170的人数 long count = list.stream() .filter(e -> e.getHeight() < 170) .count(); System.out.println("总人数" + count); //3. 请找出年龄最大的对象, 并输出(了解) Optional<Student> optional = list.stream() .max((e1, e2) -> e1.getAge() - e2.getAge()); Student student = optional.get();
//1. 请找出身高超过170的教师, 并放到一个新数组中 Object[] objects = list.stream() .filter(e -> e.getHeight() > 170) .toArray(); //2. 请找出身高超过170的教师, 并放到一个新List集合中 List<Teacher> list1 = list.stream() .filter(e -> e.getHeight() > 170) .collect(Collectors.toList()); //3. 请找出身高超过170的教师, 并放到一个新Set集合中 Set<Teacher> set = list.stream() .filter(e -> e.getHeight() > 170) .collect(Collectors.toSet()); //4. 请找出所有的教师的姓名和身高, 放到一个新Map集合中 Map<String, Double> map = list.stream() .distinct()//去重 .collect(Collectors.toMap(e -> e.getName(), e -> e.getHeight()));
IO流
用于读写数据的(可以读写文件,或网络中的数据…),I为输入流,负责把数据读到程序中,O为输出流,负责把数据写到存储中。变量数组等都是内存中的数据容器,他们记住的数据在程序终止时会丢失,因此需要存储在计算机硬盘中,也就是file文件。
File类只能对文件本身操作(如创建删除等),不能进行读写。通过 IO流 进行读写。
-
File
File对象代表 文件 或 文件夹,本质就是一个路径
绝对路径:从盘符开始。
相对路径:默认直接到当前工程下的目录寻找文件。也就是需要从模块名开始写起
// 单参数创建文件对象 File file1 = new File("D:/upload/test1"); // 多参数创建文件对象 File file2 = new File("D:/upload", "test1"); // 文件分隔符 File.separator根据系统自动匹配分隔符 new File("D:" + File.separator + "upload" + File.separator + "1.txt");
判断文件类型、获取文件信息
//boolean exists() 判断文件路径是否存在 boolean exists = f1.exists(); //boolean isFile() 判断是否是文件 boolean isfile = f1.isFile(); //boolean isDirectory() 判断是否是文件夹 boolean directory = f1.isDirectory(); //String getName() 获取文件/文件名,包含后缀 String name = f1.getName(); //long length() 获取文件大小,返回字节个数 long length = f1.length(); //long lastModified() 获取最后修改时间 long modified = f2.lastModified(); //string getPath() 获取创建对象时的路径 f1.getPath(); //String getAbsolutePath() 获取对象绝对路径 f1.getAbsolutePath();
创建删除文件、文件夹
//boolean mkdir() 创建单级文件夹,创建失败返回false boolean mkdir = f2.mkdir(); //boolean mkdirs() 创建多级文件夹 (常用) boolean mkdirs = f2.mkdirs(); //boolean createNewFile() 创建文件,文件存在返回false boolean newFile = f3.createNewFile(); //boolean delete() 删除文件或空文件夹,删除失败返回false (注意: 删除方法不走回收站,填用) System.out.println(f3.delete());
注:删除方法默认不能删除非空文件夹。创建的 File对象路径,不存在的需通过 mkdirs方法创建。
遍历文件夹
File file = new File("D:/"); //String[] list() 返回文件名数组 String[] list = file.list(); //File[] listFiles() 返回文件数组 File[] files = file.listFiles();
注意事项:
- 当主调是文件,或者路径不存在时,返回null
- 当主调是空文件夹时,返回一个长度为0的数组
- 当主调是一个非空文件夹,但是没有权限访问该文件夹时,返回null
-
字符集
- 标准ASCII字符集,1个字节存储一个字符,无中文。
- GBK(汉字内码扩展规范,国标),兼容了ASCII字符集,两个字节存储一个汉字。
- Unicode字符集(统一码,也叫万国码),UTF-32,四个字节表示一个字符。
- UTF-8,Unicode字符集的一种可变长编码方案,英文字符、数字等只占1个字节,汉字字符占用3个字节。
String str = "我abcd你"; //编码: 字符-->字节 byte[] bytes = str.getBytes();//使用默认字符集编码 //设置字符集 byte[] bytes1 = str.getBytes("GBK"); //解码: 字节-->字符 String s = new String(bytes);//使用默认字符集解码 String gbk = new String(bytes1, "GBK");
注:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码
-
IO流方向及体系
-
IO流 -- 字节流
FileInputStream(文件字节输入流)
以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中来。
每次读取一个字节(读取性能差,并且读取汉字输出会乱码):
//1. 创建文件字节输入流(c_demo1.txt) 参数也可为文件对象 FileInputStream fis = new FileInputStream("day08/c_demo1.txt"); //2. 读取文件中的内容 //使用while循环,对文件读取,每次读取一个字节返回, 如果发现没有数据可读会返回-1 int read; while ((read = fis.read()) != -1){ System.out.println(read); } //关闭流 fis.close();
每次读取多个字节(读取性能得到了提升,但读取汉字输出还是会乱码):
FileInputStream fis = new FileInputStream("day08/c_demo2.txt"); byte[] bytes = new byte[3]; int len; //每次读取字节数组长度个字节,将读取到的放入数组,发现没有数据可读会返回-1. while ((len = fis.read(bytes)) != -1){ //解码后输出(字节 -> 字符) System.out.println(new String(bytes,0,len)); } fis.close();
一次读取完全部字节(如果文件过大,字节数组也会过大,可能引起内存溢出):
//可以定义一个与文件大写一样的字节数组 //byte[] bytes = new byte[(int) file.length()]; File file = new File("day08/c_demo3.txt"); //直接使用readAllBytes将文件中所有内容读取到数组 byte[] allBytes = fis.readAllBytes(); System.out.println(new String(allBytes)); fis.close;
FileOutputStream(文件字节输出流)
以内存为基准,把内存中的数据以字节的形式写出到文件中去。注意注释前三行
//创建文件字节输出流(c_demo4.txt) //可再加一个boolean参数,若为true,则是在文件中追加内容;若为false,或不写,则是覆盖 //在单个流内,写多个write是往后追加,不会覆盖 FileOutputStream fos = new FileOutputStream("day08/c_demo4.txt"); //写一个字节进去 fos.write(97); byte[] bytes = {97,98,99,100}; //写一个字节数组进去 fos.write(bytes); //写3个,从0索引开始 fos.write(bytes,0,3); //输入换行 String str1 = "\r\n"; //使用默认字符集编码 fos.write(str1.getBytes()); fos.close;
案例一般写法
//输入流 从磁盘往内存输入 FileInputStream fis = new FileInputStream("D:/000/pic.png"); //输出流 输出到磁盘 FileOutputStream fos = new FileOutputStream("D:/000/111/pic.png"); //创建1Mb数组 byte[] bytes = new byte[1024 * 1024]; int len; //读入字节数组,将文件中字节存入数组,返回存入的数量 while ((len = fis.read(bytes)) != -1){ //输出字节数组到输出流 fos.write(bytes,0,len); } fis.close(); fos.close();
-
IO流 -- 字符流
FileReader(文件字符输入流)
以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。
//创建FileReader对象 FileReader reader = new FileReader("day09/a-1.txt"); char[] chars = new char[2]; int len; //每次读取字符数组长度的内容,放到数组中 while ((len = reader.read(chars)) != -1){ String str = new String(chars, 0, len); System.out.println(str); } // 关闭流 reader.close();
FileWriter(文件字符输出流)
以内存为基准,把内存中的数据以字符的形式写出到文件中去。
//读(输入流)文件1 写(输出流)文件2, FileWriter writer = new FileWriter("day09/a-2.txt"); FileReader reader = new FileReader("day09/a-1.txt"); char[] chars = new char[2]; int len; while ((len = reader.read(chars)) != -1){//读取数组长度的字符,放入数组中 //将数组中从索引0开始len长度的内容写入文件2 writer.write(chars,0,len); } // 关闭流 reader.close(); writer.close(); //刷新流 //writer.flush
注:
- 字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效
- 字节流适合做一切文件数据的拷贝(音视频,文本);字节流不适合读取中文内容输出。
- 字符流适合做文本文件的操作(读,写)。
-
IO流 -- 缓冲流
对原始流进行包装,自带8kb缓冲池,提高原始流读写数据的性能。因为当文件特别大时,一次性读写可能会溢出;又因从磁盘到内存耗时长,一点一点读写效率太低。
缓冲流是先将8kb数据拿到内存,再从内存一点一点往过送,避免溢出,又增加效率。
BufferedInputStream & BufferedOutputStream(字节缓冲输入流和输出流)
//创建一个缓冲字节输入流,读取文件 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\1.wmv")); //创建一个缓冲字节输出流,写文件 BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\2.wmv")); //一边读一边写 byte[] bytes = new byte[1024]; //1k int len; while ((len = bis.read(bytes)) != -1) { bos.write(bytes,0,len); } bos.close(); bis.close();
BufferedReader & BufferedWriter(字符缓冲输入流和输出流)
//创建缓冲字符输入流 , 读取b-3.txt BufferedReader reader = new BufferedReader(new FileReader("day09/b-3.txt")); //创建缓冲字符输出流, 写入b-4.txt BufferedWriter writer = new BufferedWriter(new FileWriter("day09/b-4.txt")); //循环读b-3.txt , 写入b-4.txt //初始变量,默认null String line = null; //读取一行数据返回,如果没有数据可读了,会返回null,字符缓冲输入流特有方法 while((line = reader.readLine()) != null){ writer.write(line); //换行 writer.newLine(); } //关闭流 writer.close(); reader.close();
-
IO流 -- 转换流
如果代码编码和被读取的文本文件的编码是不一致的,使用字符流读取文本文件时就会出现乱码。但JDK11开始,可直接使用 字符流构造指定字符集,构造第二个参数,通过Charset的静态方法forName 指定字符集,不用转换流。
InputStreamReader & InputStreamReader(字符输入输出转换流)
直接将原始的字节流和目标字符集作为参数编码封装进去,就可以做到转换。
但jdk11之后更简便:
FileWriter fileWriter = new FileWriter("day09/c-2.txt", Charset.forName("GBK")); BufferedWriter writer = new BufferedWriter(fileWriter); writer.write("你好"); writer.close();
-
IO流 -- 打印流
使用方便,性能高效。PrintStream 继承自字节输出流OutputStream,因此支持写字节数据的方法。PrintWriter 继承自字符输出流Writer,因此支持写字符数据出去。
//打印任意类型的数据出去 public void println(Xxx xx) //可以支持写字节数据出去(PrintStream) public void write(int/byte[]/byte[]一部分) //可以支持写字符数据出去(PrintWriter) public void write(int/String/char[]/..)
-
IO流 -- 数据流
DataOutputStream & DataInputStream(数据输出流和输入流)
为了保存数据而用的一种数据流,数据流输出的数据不是给人看的,是为了保存数据。输出流输出的数据,只能通过数据输入流读回程序。
-
IO流 -- 序列化流
ObjectOutputStream & ObjectInputStream(对象字节输出流和输入流)
方法与数据流的类似,为writeObject(),readObject()
- 序列化:将内存中的对象保存到磁盘文件
- 反序列化:将磁盘文件中的数据还原成内存对象
//准备一个Student对象 Student student = new Student("张三", 18); //将对象写入到文件中(序列化) //创建ObjectOutputStream ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day09/f-1.txt")); oos.writeObject(student); oos.close(); //反序列化,将文件中数据,还原成java对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day09/f-1.txt")); Student student1 = (Student) ois.readObject(); System.out.println(student1); ois.close();
注意:对象如果要参与序列化,类必须实现 序列化接口 Serializable
-
IO框架 Commons-io
常用工具类FileUtils public static void copyFile(File数据源,File目的地) 复制文件 public static void copyDirectory(File 数据源,File目的地) 复制文件夹 public static void deleteDirectory(File 目标文件夹) 删除文件夹(有内容也直接删) public static String readFileToString(File 数据源,String encoding) 读数据 public static void writeStringToFile(File file,String data,String encoding, Boolean append) 写数据常用工具类IOUtils类 public static int copy(InputStream inputStream, OutputStream outputStream) 复制文件 public static int copy(Reader reader, Writer writer) 复制文件 public static void write(String data, OutputStream output, String charsetName) 写数据
释放资源
try-catch-finally
try{ ... }catch(Excption e){ e.printStackTrace(); }finally{ }
finally代码区的特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。一般用于在程序执行完成后进行资源的释放操作。
try-with-resource (JDK7开始提供)
try(定义资源1;定义资源2;…){ 可能出现异常的代码; }catch(异常类名 变量名){ 异常的处理代码; }
该方式声明的资源使用完毕后,会自动调用资源close()方法,完成释放。
配置文件
有很多需要灵活配置的数据,如果在Java代码中写死,很不方便,因此可以把其放在一些文本文件中,这些文件统称为配置文件。
配置文件一般要求有 明确的格式,以方便读写操作
-
Properties
直译为属性,属于Map集合(键值对集合),但一般用来代表属性文件,而不会当集合使用。
//1. 创建一个配置文件对象(day10/a.properties) Properties properties = new Properties(); //加载配置文件 properties.load(new FileInputStream("day10/a.properties")); //根据key获取属性 String name = properties.getProperty("name"); //获取properties中所有的key Set<String> set = properties.stringPropertyNames(); //向properties中存入k/v数据 properties.setProperty("name","abc123"); properties.setProperty("password","123456"); //将数据写入properties文件中 第二个参数是注释 properties.store(new FileOutputStream("day10/b.properties"),"by xiaoming");
-
XML
可扩展标记语言,本质是一种数据的格式,可以用来存储复杂的数据结构,和数据关系。可以将对象以列表的形式存入。经常用来做为系统的配置文件;或者作为一种特殊的数据结构,在网络中进行传输。
- 文档声明必须是第一行
- XML中只能有一个根标签。
- XML中的标签可以有属性。
- 如果一个文件中放置的是XML格式的数据,这个文件就是XML文件,后缀一般要写成.xml。
- XML中书写”<”、“&”等,可能会出现冲突,但是CDATA的数据区:
<![CDATA[ …内容… ]]>,里面的内容可以随便写。
解析XML文件
不需要写原始的IO流代码来解析XML,难度较大,也相当繁琐。可用Dom4j。
DOM解析思想就是一层一层的进入,一层一层的解析,将文档的各个组成部分看做是对应的对象。不建议用Dom4j把数据写入XML文件,推荐字符串拼接然后用IO流写进去。
//创建SAXReader对象,用于解析XML文件 SAXReader saxReader = new SAXReader(); //调用SAXReader的read方法,传入需要解析的XML路径,获取Document对象 Document document = saxReader.read("day10/b-2.xml"); //通过文档对象,获取根元素 Element rootElement = document.getRootElement(); //获取根节点下的所有user子元素 List<Element> users = rootElement.elements("user"); //遍历users,获取每个user子元素 for (Element user : users) { //获取user的id属性 String id = user.attributeValue("id"); //获取user下的name等元素 Element name = user.element("name"); Element password = user.element("password"); Element address = user.element("address"); Element gender = user.element("gender"); //获取文本内容 Student student = new Student(id, name.getText(), password.getText(), address.getText(), gender.getText()); }
总:两种配置文件的读写,都需要先创建对象, 然后将文件加载进对象中,但XML还需要层层进入才能获取。两者相比properties只能存放键值对类型的,XML可以存放复杂数据类型。
日志
本质上就是一个记录程序运行过程中的各种信息的文件,可通过其中的信息进行分析定位。相对输出语句来说,可灵活记录到文件、数据库中,且可以以开关形式控制日志的启停,无需修改源代码。
Logback是基于SLF4J的日志规范实现的框架,其性能优于Log4j。
实现步骤
- 使用Logback日志框架,至少需要在项目中整合三个模块:SLF4J-api(日志接口)、logback-core(基础/核心模块)、logback-classic(功能模块,完整实现了slf4j API的模块)
- 将Logback框架的核心配置文件logback.xml直接拷贝到src目录下(必须是src下)
- 创建Logback框架提供的Logger对象,然后用Logger对象调用其提供的方法就可以记录系统的日志信息。
//创建Logger对象 public static final Logger LOGGER = LoggerFactory.getLogger(Demo1.class); public static void main(String[] args) { divide(10, 0); } private static int divide(int num1, int num2) { //记录:需要执行方法 LOGGER.warn("开始执行divide方法,被除数如果是0,可能有问题"); //记录:方法执行的参数 LOGGER.debug("传入的参数-被除数:" + num1 + ",除数:" + num2); //记录:方法执行的结果 try { int res = num1/num2; LOGGER.info("执行的结果:"+res); LOGGER.debug("传入的参数-被除数:"+num1 + ",除数:"+num2); return res; }catch (Exception e){ LOGGER.error("对不起,错了!"); return 0; } }
日志级别
日志级别由上到下依次升高,在配置文件中,修改属性level的值,可以控制日志启动的最低级别。默认级别是debug,不区分大小写。若设置为info,则trace和debug不会被记录。ALL 和 OFF分别是打开全部日志和关闭全部日志。
总:导入框架依赖jar包,然后在配置文件中指定日志输出位置和输出级别,最后在代码中用Logger对象的级别方法传入语句就可。
多线程
线程和进程
进程是一个完整的运行程序,打开的客户端都是进程,多线程指计算机同一时间可以做多件事。进程中可以有多个线程。
严格意义上来说多个线程并不是同时运行,而是CPU在多个线程间快速切换,其作用是可以提高CPU的利用率,减少CPU空闲时间。如磁盘读写等操作需要等待I/O,如果单线程,CPU会处于空闲。 也能进行并发处理,看起来像多个任务同时进行,同时处理多个用户请求。其实多线程可以类比生活中的一心多用,如人脑会在不同任务之间快速切换注意力,减少大脑空闲时间,提高任务效率。
并发和并行
并发指多个任务看起来像同时执行,并不关心是否真正同时执行,也就是单个CPU多线程的快速切换。并行指同一时刻,多个任务能够真正同时执行,也就是多核CPU。
线程创建方式
继承Thread类(重写run())
线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
注意:启动线程必须是调用start方法,不是调用run方法。若主线程任务写在启动线程之后,那它与其他线程会并发执行。
public static void main(String[] args) { //需求:创建两个线程,分别用于打印100个A和100个B,最后观察下输出顺序 //创建一个线程对象 AThread aThread = new AThread(); BThread bThread = new BThread(); //调用start方法,启动线程,背后执行 aThread.start(); bThread.start(); } //创建线程对象,在run方法中配置需要执行的任务 class AThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("A" +i); } } } class BThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("B" +i); } } }
实现Runnable接口(重写run())
任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。但是不能返回线程执行结果。
public static void main(String[] args) { //需求:创建两个线程,分别用于打印10个A和10个B,最后观察下输出顺序 //创建任务对象 ARunnable aRunnable = new ARunnable(); BRunnable bRunnable = new BRunnable(); //调用Thread类的,有参构造,初始化一个线程对象 Thread aThread = new Thread(aRunnable); Thread bThread = new Thread(bRunnable); //多用线程的start方法启动线程 aThread.start(); bThread.start(); } class ARunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("A" + i); } } } class BRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("B" + i); } } }
//匿名内部类写法 public static void main(String[] args) { Thread aThread = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("A" + i); } }); Thread bThread = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("B" + i); } }); aThread.start(); bThread.start(); }
实现Callable接口(重写call方法)
利用Callable接口、FutureTask类来实现。
定义类 实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。把Callable类型的对象 封装成FutureTask(线程任务对象)。然后把线程任务对象交给Thread对象。调用Thread对象的 start方法启动 线程。最后通过 FutureTask对象的的get方法 去获取线程任务执行的结果。在返回出结果之前处于等待状态。
public static void main(String[] args) throws Exception{ //需求:启动两个子线程,分别计算100之内的奇数的和和偶数的和,然后在主线程中再做个汇总,得到总和 //创建Callable对象 OddCallable oddCallable = new OddCallable(); EvenCallable evenCallable = new EvenCallable(); //封装成FutureTask任务对象 FutureTask<Integer> oddFutureTask = new FutureTask<>(oddCallable); FutureTask<Integer> evenFutureTask = new FutureTask<>(evenCallable); //使用Thread的构造传入任务对象,创建Thread Thread oddThread = new Thread(oddFutureTask); Thread evenThread = new Thread(evenFutureTask); oddThread.start(); evenThread.start(); Integer odd = oddFutureTask.get(); Integer even = evenFutureTask.get(); System.out.println("和:" + (odd+even)); } //创建A线程,计算奇数的和,并返回 class OddCallable implements Callable<Integer>{ //重写call方法,封装要做的事情,和要返回的数据 @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i+=2) { sum += i; } return sum; } } //创建B线程,计算偶数的和,并返回 class EvenCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i+=2) { sum += i; } return sum; } }
Thread常用方法
-
线程的生命周期和状态
- NEW,新建状态。线程刚被创建,但是并未启动。
- RUNNABLE,就绪状态。线程已经调用了start(),等待CPU调度。
- 运行状态。
- TERMINATED,销毁状态。因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
- BLOCKED,阻塞状态。线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态。
- WAITING,等待状态。一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 TIMED_WAITING。
- 计时等待状态,调用sleep(...),进入此状态,不释放线程,休息指定时间后继续进行。
-
线程安全
线程安全问题指的是当多个线程同时操作同一个共享资源的时候,可能会出现的操作结果不符预期问题。例如两人共同账户10万元,两人同时各取钱10万,可能都会取出来,这就是出现了同时进行的线程安全问题。
-
线程同步方案
多个线程实现先后依次访问共享资源,最常见的方案就是加锁,每次只允许一个线程进入。
同步代码块
把访问共享资源的核心代码给上锁。实例方法建议使用 this 作为锁对象,静态方法建议使用字节码(类名.class)对象作为锁对象。也就是说同一实例的多个线程只能一个一个执行。
synchronized(同步锁) { 访问共享资源的核心代码 }
同步方法
同步方法其实底层也是有隐式锁对象的,与同步代码块一致。
修饰符 synchronized 返回值类型 方法名称(形参列表) { 操作共享资源的代码 }
Lock锁
可以创建出锁对象进行加锁和解锁,更灵活、更方便。
Lock是接口,不能直接实例化,采用它的实现类 ReentrantLock来构建Lock锁对象。
注:定义Lock锁需要用 final。
//定义锁对象 private final Lock lock = new ReentrantLock(); try { //加锁 lock.lock(); 操作共享资源代码 ... }catch (Exception e) { e.printStackTrace(); }finally { //解锁 lock.unlock(); }
-
线程池
可以复用线程的技术,也就是提前创建出一批线程,减少大量的创建和销毁线程请求。
使用线程池ExecutorService的实现类:ThreadPoolExecutor 自创建线程池对象。
执行流程
- 判断核心线程数是否已满,如果没满,则创建一个新的核心线程来执行任务
- 如果核心线程满了,则判断工作队列是否已满,如果没满,则将任务存储在工作队列
- 如果工作队列满了,则判断最大线程数是否已满,如果没满,则创建临时线程执行任务
- 如果最大线程数已满,则执行拒绝策略
线程池的创建和执行
七个参数
- 核心线程的数量
- 最大线程数量
- 临时线程的存活时间
- 指定临时线程存活的时间单位(秒、分、时、天)
- 任务队列(阻塞队列)
- 线程工厂(创建线程)
- 任务拒绝策略
//创建一个线程池 ThreadPoolExecutor executorService = new ThreadPoolExecutor( 3, 5, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); //线程池处理Runnable任务 //创建任务对象,交给线程池,调度运行 executorService.execute(new ARunnable()); executorService.execute(new ARunnable()); //等全部任务执行完毕后,再关闭线程池 executorService.shutdown(); //线程池处理Callable任务 //创建任务对象,提交到线程池 Future<Integer> future = executorService.submit(new SumCallable(5)); try { Integer sum = future.get(); System.out.println(sum); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executorService.shutdown(); //创建Runnable任务 class ARunnable implements Runnable { @Override public void run() { String name = Thread.currentThread().getName(); System.out.println(name + "执行了任务"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } //创建Callable任务 class SumCallable implements Callable<Integer> { private int num; public SumCallable() { } public SumCallable(int num) { this.num = num; } @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= num; i++) { sum += i; } return sum; } }