java最全基础③进阶
final
1、final关键字
1.1、final修饰的类无法继承。 String就是final修饰 无法继承(成员变量才有初始值 局部变量没有初始值) new 构造方法的时候默认赋值
1.2、final修饰的方法无法覆盖。
1.3、final修饰的变量只能赋一次值。
1.4、final修饰的引用一旦指向某个对象,则不能再重新指向其它对象,但该引用,该对象不会被垃圾回收器回收除非方法结束
指向的对象内部的数据是可以修改的。
1.5、final修饰的实例变量必须手动初始化,不能采用系统默认值。
1.6、final修饰的实例变量一般和static联合使用,称为常量。(常量和静态变量都存放在方法区 都在类加载时初始化)
public static final double PI = 3.1415926;
抽象类
1、什么是抽象类?
类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。
类本身是不存在的,所以抽象类无法创建对象《无法实例化》。
2、抽象类属于什么类型?
抽象类也属于引用数据类型。
3、抽象类怎么定义?《能把基础语法先学会》
语法:
[修饰符列表] abstract class 类名{
类体;
}
4、抽象类是无法实例化的,无法创建对象的,所以抽象类是用来被子类继承的。
5、final和abstract不能联合使用,这两个关键字是对立的。
6、抽象类的子类可以是抽象类。也可以是非抽象类。
7、抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的。
8、抽象类关联到一个概念:抽象方法。什么是抽象方法呢?
抽象方法表示没有实现的方法,没有方法体的方法。例如:
public abstract void doSome();
抽象方法特点是:
特点1:没有方法体,以分号结尾。
特点2:前面修饰符列表中有abstract关键字。
9、抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。
public class AbstractTest01{ public static void main(String[] args){// 错误: Account是抽象的; 无法实例化//Account act = new Account();}
}// 银行账户类
//错误: 非法的修饰符组合: abstract和final
/*
final abstract class Account{}
*/abstract class Account{/*public Account(){}public Account(String s){}*/// 非抽象方法public void doOther(){}// 抽象方法public abstract void withdraw();
}// 子类继承抽象类,子类可以实例化对象
/*
class CreditAccount extends Account{public CreditAccount(){super();}
}
*/// 抽象类的子类可以是抽象类吗?可以
/*
abstract class CreditAccount extends Account{}
*/
抽象类:
1、抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中。
2、重要结论:重要结论五颗星*****(必须记住)
一个非抽象的类继承抽象类,必须将抽象类中的抽象方法实现了。
这是java语法上强行规定的,必须的,不然编译器就报错了。
这里的覆盖或者说重写,也可以叫做实现。(对抽象的实现。)
public class AbstractTest02{public static void main(String[] args){// 能不能使用多态?// 父类型引用指向子类型对象。Animal a = new Bird(); // 向上转型。(自动类型转换)// 这就是面向抽象编程。// 以后你都是调用的a.XXXX// a的类型是Animal,Animal是抽象的// 面向抽象编程,不要面向具体编程,降低程序的耦合度,提高程序的扩展力。// 这种编程思想符合OCP原则。/*分析以下:编译的时候这个move()方法是谁的?运行的时候这个move()方法又是谁的?*/a.move();// 多态(当对多态不是很理解的时候,以后写代码能用多态就用多态。慢慢就理解了。)Animal x = new Cat();x.move();}
}// 动物类(抽象类)
abstract class Animal{// 抽象方法public abstract void move();
}// 子类(非抽象的)
// 错误: Bird不是抽象的, 并且未覆盖Animal中的抽象方法move()
/*
class Bird extends Animal{
}
*/class Bird extends Animal{// 需要将从父类中继承过来的抽象方法进行覆盖/重写,或者也可以叫做“实现”。// 把抽象的方法实现了。public void move(){System.out.println("鸟儿在飞翔!");}
}class Cat extends Animal{public void move(){System.out.println("猫在走猫步!");}
}
面试题(判断题):java语言中凡是没有方法体的方法都是抽象方法。不对,错误的。Object类中就有很多方法都没有方法体,都是以“;”结尾的,但他们都不是抽象方法,例如:public native int hashCode();这个方法底层调用了C++写的动态链接库程序。前面修饰符列表中没有:abstract。有一个native。表示调用JVM本地程序。
interface
接口:
1、接口也是一种“引用数据类型”。编译之后也是一个class字节码文件。
2、接口是完全抽象的。(抽象类是半抽象。)或者也可以说接口是特殊的抽象类。
3、接口怎么定义,语法是什么?
[修饰符列表] interface 接口名{}
4、接口支持多继承,一个接口可以继承多个接口。
5、接口中只包含两部分内容,一部分是:常量。一部分是:抽象方法。接口中没有其它内容了。只有以上两部分。
6、接口中所有的元素都是public修饰的。(都是公开的。)
7、接口中的抽象方法定义时:public abstract修饰符可以省略。
8、接口中的方法都是抽象方法,所以接口中的方法不能有方法体。
9、接口中的常量的public static final可以省略。
public class Test01{public static void main(String[] args){// 访问接口的常量。System.out.println(MyMath.PI);// 常量能重新赋值吗?//错误: 无法为最终变量PI分配值//MyMath.PI = 3.1415928;//错误: 无法为最终变量k分配值//MyMath.k = 111;}
}// 定义接口
interface A{}// 接口支持继承
interface B extends A{}// 一个接口可以继承多个接口(支持多继承)
interface C extends A, B{}// 我的数学接口
interface MyMath{// 常量//public static final double PI = 3.1415926;// public static final可以省略吗?double PI = 3.1415926;// k是不是常量????是。// 接口中随便写一个变量就是常量。// 常量:值不能发生改变的变量。int k = 100;// 抽象方法//public abstract int sum(int a, int b);// 接口当中既然都是抽象方法,那么在编写代码的时候,public abstract可以省略吗?int sum(int a, int b);// 接口中的方法可以有方法体吗?// 错误: 接口抽象方法不能带有主体/*void doSome(){}*/// 相减的抽象方法int sub(int a, int b);}
接口的基础语法:
1、类和类之间叫做继承,类和接口之间叫做实现。
别多想:你仍然可以将"实现"看做“继承”。
继承使用extends关键字完成。
实现使用implements关键字完成。
2、五颗星(*****):当一个非抽象的类实现接口的话,必须将接口中所有的抽象方法全部实现(覆盖、重写)。
public class Test02{public static void main(String[] args){//错误: MyMath是抽象的; 无法实例化//new MyMath();// 能使用多态吗?可以。//Animal a = new Cat();// 父类型的引用指向子类型的对象MyMath mm = new MyMathImpl();// 调用接口里面的方法(面向接口编程。)int result1 = mm.sum(10, 20);System.out.println(result1);int result2 = mm.sub(20, 10);System.out.println(result2);}
}// 特殊的抽象类,完全抽象的,叫做接口。
interface MyMath{double PI = 3.1415926;int sum(int a, int b);int sub(int a, int b);
}// 这样没问题
/*
abstract class MyMathImpl implements MyMath {
}
*/// 编写一个类(这个类是一个“非抽象”的类)
// 这个类的名字是随意的。
//错误: MyMathImpl不是抽象的, 并且未覆盖MyMath中的抽象方法sub(int,int)
/*
class MyMathImpl implements MyMath {
}
*///修正
class MyMathImpl implements MyMath {//错误:正在尝试分配更低的访问权限; 以前为public/*int sum(int a, int b){return a + b;}*/// 重写/覆盖/实现 接口中的方法(通常叫做实现。)public int sum(int a, int b){return a + b;}public int sub(int a, int b){return a - b;}
}
重点(五颗星*****):一个类可以同时实现多个接口。
之前有一个结论:
无论向上转型还是向下转型,两种类型之间必须要有继承关系,
没有继承关系编译器会报错。(这句话不适用在接口方面。)
最终实际上和之前还是一样,需要加:instanceof运算符进行判断。
向下转型养成好习惯。转型之前先if+instanceof进行判断。
D -> A
D -> B
D -> C
// 多态该怎么用呢?// 都是父类型引用指向子类型对象A a = new D();//a.m2(); // 编译报错。A接口中没有m2()方法。B b = new D();C c = new D();// 这个编译没问题,运行也没问题。// 调用其他接口中的方法,你需要转型(接口转型。)B b2 = (B)a;b2.m2();// 直接向下转型为D可以吗?可以D d = (D)a;d.m2();
继承和实现都存在的话,代码应该怎么写?
extends 关键字在前。
implements 关键字在后。
class Cat extends Animal implements Flyable{public void fly(){System.out.println("飞猫起飞,翱翔太空的一只猫,很神奇,我想做一只猫!!");}
}// 创建对象(表面看Animal类没起作用!)Flyable f = new Cat(); //多态。f.fly();// 同一个接口Flyable f2 = new Pig();// 调用同一个fly()方法,最后的执行效果不同。f2.fly();Flyable f3 = new Fish();f3.fly();
经典案例
调用者面向接口调用。(顾客)
实现者面向接口编写实现。(厨师)
public interface FoodMenu{// 西红柿炒蛋void shiZiChaoJiDan();// 鱼香肉丝void yuXiangRouSi();}public class AmericCooker implements FoodMenu{// 西红柿炒蛋public void shiZiChaoJiDan(){System.out.println("西餐师傅做的西红柿炒鸡蛋!");}// 鱼香肉丝public void yuXiangRouSi(){System.out.println("西餐师傅做的鱼香肉丝!");}
}public class ChinaCooker implements FoodMenu{// 西红柿炒蛋public void shiZiChaoJiDan(){System.out.println("中餐师傅做的西红柿炒鸡蛋,东北口味!");}// 鱼香肉丝public void yuXiangRouSi(){System.out.println("中餐师傅做的鱼香肉丝,东北口味!");}
}
public class Customer{// 顾客手里有一个菜单// Customer has a FoodMenu!(这句话什么意思:顾客有一个菜单)// 记住:以后凡是能够使用 has a 来描述的,统一以属性的方式存在。// 实例变量,属性// 面向抽象编程,面向接口编程。降低程序的耦合度,提高程序的扩展力。private FoodMenu foodMenu; // 如果以下这样写,就表示写死了(焊接了。没有可插拔了。)// 中餐厨师//ChinaCooker cc;// 西餐厨师//AmericCooker ac// 构造方法 }public Customer(FoodMenu foodMenu){this.foodMenu = foodMenu;}// setter and getterpublic void setFoodMenu(FoodMenu foodMenu){this.foodMenu = foodMenu;}public FoodMenu getFoodMenu(){return foodMenu;}// 提供一个点菜的方法public void order(){// 先拿到菜单才能点菜// 调用get方法拿菜单。//FoodMenu fm = this.getFoodMenu();// 也可以不调用get方法,因为在本类中私有的属性是可以访问foodMenu.shiZiChaoJiDan();foodMenu.yuXiangRouSi();}
}
public class Test{public static void main(String[] args){// 创建厨师对象//FoodMenu cooker1 = new ChinaCooker();FoodMenu cooker1 = new AmericCooker();// 创建顾客对象Customer customer = new Customer(cooker1);// 顾客点菜customer.order();}
}
类型和类型之间的关系:
is a(继承)、has a(关联)、like a(实现)
is a:
Cat is a Animal(猫是一个动物)
凡是能够满足is a的表示“继承关系”
A extends B
has a:
I has a Pen(我有一支笔)
凡是能够满足has a关系的表示“关联关系”
关联关系通常以“属性”的形式存在。
A{
B b;
}
like a:
Cooker like a FoodMenu(厨师像一个菜单一样)
凡是能够满足like a关系的表示“实现关系”
实现关系通常是:类实现接口。
A implements B
抽象和接口的区别
抽象 | 接口 |
---|---|
抽象类是半抽象的。 | 接口是完全抽象的。 |
抽象类中有构造方法。 | 接口中没有构造方法。 |
类和类之间只能单继承。 | 接口和接口之间支持多继承。 |
一个抽象类只能继承一个类(单继承)。 | 一个类可以同时实现多个接口。 |
接口中只允许出现常量和抽象方法。 |
访问权限
访问控制修饰符 | 本类 | 同包 | 子类 | 任意位置 |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 可以 | 不行 |
默认 | 可以 | 可以 | 不行 | 不行 |
private | 可以 | 不行 | 不行 | 不行 |
1.3、访问控制权限修饰符可以修饰什么?
属性(4个都能用)
方法(4个都能用)
类(public和默认能用,其它不行。)
接口(public和默认能用,其它不行。)
Object
toString
1、源代码长什么样?
public String toString() {
return this.getClass().getName() + “@” + Integer.toHexString(hashCode());
}
System.out.println(引用); 这里会自动调用“引用”的toString()方法。
//MyTime类重写toString()方法之前//System.out.println(s1); // MyTime@28a418fc//MyTime类重写toString()方法之后System.out.println(s1); // 1970年1月1日//System.out.println(t1.toString()); //1970年1月1日// 注意:输出引用的时候,会自动调用该引用的toString()方法。System.out.println(t1);
equals
基本数据类型比较实用:,“==”判断的是两个java对象的内存地址
// 判断两个基本数据类型的数据是否相等直接使用“==”就行。int a = 100;int b = 100;// 这个“==”是判断a中保存的100和b中保存的100是否相等。System.out.println(a == b); //true(相等)
对象和对象比较:调用equals方法
MyTime t1 = new MyTime(2008, 8, 8); //MyTime t1 = 0x1234;// 创建了一个新的日期对象,但表示的日期也是:2008年8月8日。MyTime t2 = new MyTime(2008, 8, 8); //MyTime t2 = 0x3698;//测试以下,比较两个对象是否相等,能不能使用“==”???// 这里的“==”判断的是:t1中保存的对象内存地址和t2中保存的对象内存地址是否相等。System.out.println(t1 == t2); // false
// 重写Object equals方法之后(比较的是内容。)boolean flag = t1.equals(t2);System.out.println(flag); //true
String类
1、String类已经重写了equals方法,比较两个字符串不能使用==,必须使用equals。
equals是通用的。
String s1 = "hello"; == 是trueString s2 = "abc";String s3 = new String("Test1");String s4 = new String("Test1"); == 是false
2、String类已经重写了toString方法。
大结论:
java中什么类型的数据可以使用“== ”判断
java中基本数据类型比较是否相等,使用 ==
java中什么类型的数据需要使用equals判断
java中所有的引用数据类型统一使用equals方法来判断是否相等。
finalize
1、在Object类中的源代码:
protected void finalize() throws Throwable { }
GC:负责调用finalize()方法。
2、finalize()方法只有一个方法体,里面没有代码,而且这个方法是protected修饰的。
3、这个方法不需要程序员手动调用,JVM的垃圾回收器负责调用这个方法。
不像equals toString,equals和toString()方法是需要你写代码调用的。
finalize()只需要重写,重写完将来自动会有程序来调用。
4、finalize()方法的执行时机:
当一个java对象即将被垃圾回收器回收的时候,垃圾回收器负责调用
finalize()方法。
内部类
class Test01{内部类的分类:静态内部类:类似于静态变量实例内部类:类似于实例变量局部内部类:类似于局部变量// 静态变量static String country;// 该类在类的内部,所以称为内部类// 由于前面有static,所以称为“静态内部类”static class Inner1{}// 实例变量int age;// 该类在类的内部,所以称为内部类// 没有static叫做实例内部类。class Inner2{}// 方法public void doSome(){// 局部变量int i = 100;// 该类在类的内部,所以称为内部类// 局部内部类。class Inner3{}}public void doOther(){// doSome()方法中的局部内部类Inner3,在doOther()中不能用。}// main方法,入口public static void main(String[] args){// 调用MyMath中的mySum方法。MyMath mm = new MyMath();/*Compute c = new ComputeImpl();mm.mySum(c, 100, 200);*///合并(这样写代码,表示这个类名是有的。类名是:ComputeImpl)//mm.mySum(new ComputeImpl(), 100, 200);// 使用匿名内部类,表示这个ComputeImpl这个类没名字了。// 这里表面看上去好像是接口可以直接new了,实际上并不是接口可以new了。// 后面的{} 代表了对接口的实现。// 不建议使用匿名内部类,为什么?// 因为一个类没有名字,没有办法重复使用。另外代码太乱,可读性太差。mm.mySum(new Compute(){public int sum(int a, int b){return a + b;}}, 200, 300);}}
静态内部类
public static void main(String[] args){// 调用MyMath中的mySum方法。MyMath mm = new MyMath();/*Compute c = new ComputeImpl();mm.mySum(c, 100, 200);*///合并(这样写代码,表示这个类名是有的。类名是:ComputeImpl)//mm.mySum(new ComputeImpl(), 100, 200);// 使用匿名内部类,表示这个ComputeImpl这个类没名字了。// 这里表面看上去好像是接口可以直接new了,实际上并不是接口可以new了。// 后面的{} 代表了对接口的实现。// 不建议使用匿名内部类,为什么?// 因为一个类没有名字,没有办法重复使用。另外代码太乱,可读性太差。mm.mySum(new Compute(){public int sum(int a, int b){return a + b;}}, 200, 300);}}// 负责计算的接口
interface Compute{ // 抽象方法int sum(int a, int b);
}// 你自动会在这里编写一个Compute接口的实现类
/*
class ComputeImpl implements Compute{// 对方法的实现public int sum(int a, int b){return a + b;}
}
*/// 数学类
class MyMath{// 数学求和方法public void mySum(Compute c, int x, int y){int retValue = c.sum(x, y);System.out.println(x + "+" + y + "=" + retValue);}
}
数组
数组是一种引用数据类型。数组的父类是Object。
数组当中可以存储“基本数据类型”的数据,也可以存储“引用数据类型”的数据。
数组因为是引用类型,所以数组对象是堆内存当中。(数组是存储在堆当中的)
数组当中如果存储的是“java对象”的话,实际上存储的是对象的“引用(内存地址)”,数组中不能直接存储java对象。
数组一旦创建,在java中规定,长度不可变。(数组长度不可变)
所有的数组对象都有length属性(java自带的),用来获取数组中元素的个数。
优点:检索效率高。
缺点:随机增删效率较低,数组无法存储大数据量。
注意:数组最后一个元素的增删效率不受影响。
一维数组的静态初始化和动态初始化
静态初始化:
int[] arr = {1,2,3,4};
int arr1[] =new int[]{1,2,3};
Object[] objs = {new Object(), new Object(), new Object()};
动态初始化:
int[] arr = new int[4]; // 4个长度,每个元素默认值0
Object[] objs = new Object[4]; // 4个长度,每个元素默认值null
一维数组的遍历
for(int i = 0; i < arr.length; i++){
System.out.println(arr[i]);
}
// 如果直接传递一个静态数组的话,语法必须这样写。printArray(new int[]{1,2,3});// 动态初始化一维数组int[] a2 = new int[4];printArray(a2);
main方法上面的“String[] args”有什么用?
分析以下:谁负责调用main方法(JVM)
JVM调用main方法的时候,会自动传一个String数组过来。
// JVM默认传递过来的这个数组对象的长度?默认0// 通过测试得出:args不是null。System.out.println("JVM给传递过来的String数组参数,它这个数组的长度是?" + args.length);
Animal[] ans = new Animal[2];
ans[0] = new Animal(); //可以直接放子类
ans[1] = new Cat();
class Animal{public void move(){System.out.println("Animal move...");}
}class Cat extends Animal {public void move(){System.out.println("猫在走猫步!");}// 特有方法public void catchMouse(){System.out.println("猫抓老鼠!");}
}class Bird extends Animal {public void move(){System.out.println("Bird Fly!!!");}// 特有的方法public void sing(){System.out.println("鸟儿在歌唱!!!");}
}Cat c = new Cat();Bird b = new Bird();Animal[] anis = {c, b};//Animal[] anis = {new Cat(), new Bird()}; // 该数组中存储了两个对象的内存地址。for (int i = 0; i < anis.length; i++){// 这个取出来的可能是Cat,也可能是Bird,不过肯定是一个Animal// 如果调用的方法是父类中存在的方法不需要向下转型。直接使用父类型引用调用即可。//anis[i]//Animal an = anis[i];//an.move();//Animal中没有sing()方法。//anis[i].sing();// 调用子对象特有方法的话,需要向下转型!!!if(anis[i] instanceof Cat){Cat cat = (Cat)anis[i];cat.catchMouse();}else if(anis[i] instanceof Bird){Bird bird = (Bird)anis[i];bird.sing();}}}
二维数组
int[][] a = {{100, 200, 300},{30, 20, 40, 50, 60},{6, 7, 9, 1},{0}};System.out.println(a.length); // 4System.out.println(a[0].length); // 3System.out.println(a[1].length); // 5System.out.println(a[2].length); // 4System.out.println(a[3].length); // 1// 取出第2个一维数组当中第3个元素System.out.println("第二个一维数组中第三个元素:" + a[1][2]);
// 二维数组String[][] array = {{"java", "oracle", "c++", "python", "c#"},{"张三", "李四", "王五"},{"lucy", "jack", "rose"}};// 合并代码for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)for(int j = 0; j < array[i].length; j++){System.out.print(array[i][j] + " ");}System.out.println();}
String
1、String表示字符串类型,属于引用数据类型,不属于基本数据类型。
2、在java中随便使用双引号括起来的都是String对象。例如:“abc”,“def”,“hello world!”,这是3个String对象。
3、java中规定,双引号括起来的字符串,是不可变的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变
4、在JDK当中双引号括起来的字符串,例如:“abc” "def"都是直接存储在“方法区”的“字符串常量池”当中的。
为什么SUN公司把字符串存储在一个“字符串常量池”当中呢。因为字符串在实际的开发中使用太频繁。为了执行效率,
所以把字符串放到了方法区的字符串常量池当中。
5.String 是 final的byte[] 数组
// 这两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。String s1 = "abcdef";String s2 = "abcdef" + "xy";// 分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?// 凡是双引号括起来的都在字符串常量池中有一份。// new对象的时候一定在堆内存当中开辟空间。String s3 = new String("xy");// i变量中保存的是100这个值。int i = 100;// s变量中保存的是字符串对象的内存地址。// s引用中保存的不是"abc",是0x1111// 而0x1111是"abc"字符串对象在“字符串常量池”当中的内存地址。String s = "abc";
User user = new User(110, “张三”);
String s1 = "hello";// "hello"是存储在方法区的字符串常量池当中// 所以这个"hello"不会新建。(因为这个对象已经存在了!)String s2 = "hello";// 分析结果是true还是false?// == 双等号比较的是不是变量中保存的内存地址?是的。System.out.println(s1 == s2); // trueString x = new String("xyz");String y = new String("xyz");// 分析结果是true还是false?// == 双等号比较的是不是变量中保存的内存地址?是的。System.out.println(x == y); //false
几个对象
public static void main(String[] args) {/*一共3个对象:方法区字符串常量池中有1个:"hello"堆内存当中有两个String对象。一共3个。*/String s1 = new String("hello");String s2 = new String("hello");}
String构造方法
- 第一个:String s = new String("");
- 第二个:String s = “”; 最常用
- 第三个:String s = new String(char数组);
- 第四个:String s = new String(char数组,起始下标,长度);
- 第五个:String s = new String(byte数组);
- 第六个:String s = new String(byte数组,起始下标,长度);
String s1 = "hello world!";// 但是输出一个字符串,说明String类已经重写了toString()方法。System.out.println(s1);//hello world!byte[] bytes = {97, 98, 99}; // 97是a,98是b,99是cString s2 = new String(bytes);System.out.println(s2.toString()); //abcString s3 = new String(bytes, 1, 2);System.out.println(s3); // bc// 将char数组全部转换成字符串char[] chars = {'我','是','中','国','人'};String s4 = new String(chars);System.out.println(s4);// 将char数组的一部分转换成字符串String s5 = new String(chars, 2, 3);System.out.println(s5);String s6 = new String("helloworld!");System.out.println(s6); //helloworld!
常用方法
.char charAt(int index)
char c = "中国人".charAt(1); // "中国人"是一个字符串String对象。只要是对象就能“点.”System.out.println(c); // 国
.int compareTo(String anotherString)
// 字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。int result = "abc".compareTo("abc");System.out.println(result); //0(等于0) 前后一致 10 - 10 = 0int result2 = "abcd".compareTo("abce");System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1int result3 = "abce".compareTo("abcd");System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1// 拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。System.out.println("xyz".compareTo("yxz")); // -1
.boolean contains(CharSequence s)
// 判断前面的字符串中是否包含后面的子字符串。System.out.println("HelloWorld.java".contains(".java")); // trueSystem.out.println("http://www.baidu.com".contains("https://")); // false
. boolean endsWith(String suffix) 、boolean startsWith(String prefix)
// 判断当前字符串是否以某个子字符串结尾。System.out.println("test.txt".endsWith(".java")); // falseSystem.out.println("test.txt".endsWith(".txt")); // trueSystem.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true
.boolean equals(Object anObject)
// 比较两个字符串必须使用equals方法,不能使用“==”// equals方法有没有调用compareTo方法? 老版本可以看一下。JDK13中并没有调用compareTo()方法。// equals只能看出相等不相等。// compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。System.out.println("abc".equals("abc")); // true
.boolean equalsIgnoreCase(String anotherString)
// 判断两个字符串是否相等,并且同时忽略大小写。System.out.println("ABc".equalsIgnoreCase("abC")); // true
.byte[] getBytes()
// 将字符串对象转换成字节数组byte[] bytes = "abcdef".getBytes();for(int i = 0; i < bytes.length; i++){System.out.println(bytes[i]);}
.int indexOf(String str)
// 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); // 6
.boolean isEmpty()
// 判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。//String s = "";String s = "a";System.out.println(s.isEmpty());
. int length()
判断数组长度是length属性,判断字符串长度是length()方法。
// 面试题:判断数组长度和判断字符串长度不一样// 判断数组长度是length属性,判断字符串长度是length()方法。System.out.println("abc".length()); // 3System.out.println("".length()); // 0
.int lastIndexOf(String str)
// 判断某个子字符串在当前字符串中最后一次出现的索引(下标)System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22
. String replace(CharSequence target, CharSequence replacement)
// 替换。// String的父接口就是:CharSequenceString newString = "http://www.baidu.com".replace("http://", "https://");System.out.println(newString); //https://www.baidu.com// 把以下字符串中的“=”替换成“:”String newString2 = "name=zhangsan&password=123&age=20".replace("=", ":");System.out.println(newString2); //name:zhangsan&password:123&age:20
.String[] split(String regex)
String[] ymd = "1980-10-11".split("-"); //"1980-10-11"以"-"分隔符进行拆分。for(int i = 0; i < ymd.length; i++){System.out.println(ymd[i]);}
//1980
//10
//11String param = "name=zhangsan&password=123&age=20";String[] params = param.split("&");for(int i = 0; i <params.length; i++){System.out.println(params[i]);// 可以继续向下拆分,可以通过“=”拆分。}
String substring(int beginIndex) 参数是起始下标。
// 截取字符串System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com
String substring(int beginIndex, int endIndex)
// beginIndex起始位置(包括)// endIndex结束位置(不包括)System.out.println("http://www.baidu.com".substring(7, 10)); //www
char[] toCharArray()
// 将字符串转换成char数组char[] chars = "我是中国人".toCharArray();for(int i = 0; i < chars.length; i++){System.out.println(chars[i]);}
// 我
// 是
// 中
// 国
// 人
String toLowerCase()
// 转换为小写。System.out.println("ABCDefKXyz".toLowerCase());// 19(掌握)、String toUpperCase();System.out.println("ABCDefKXyz".toUpperCase());
String trim();
// 去除字符串前后空白System.out.println(" hello world ".trim());
String中只有一个方法是静态的,不需要new对象
valueOf
// 这个方法叫做valueOf// 作用:将“非字符串”转换成“字符串”//String s1 = String.valueOf(true);//String s1 = String.valueOf(100);//String s1 = String.valueOf(3.14);// 这个静态的valueOf()方法,参数是一个对象的时候,会自动调用该对象的toString()方法吗?String s1 = String.valueOf(new Customer());//System.out.println(s1);
// 没有重写toString()方法之前是对象内存地址 com.bjpowernode.javase.string.Customer@10f87f48System.out.println(s1); //我是一个VIP客户!!!!
StringBuffer
如果以后需要进行大量字符串的拼接操作,建议使用JDK中自带的:
java.lang.StringBuffer
java.lang.StringBuilder
如何优化StringBuffer的性能?
在创建StringBuffer的时候尽可能给定一个初始化容量。(原来的空间释放)
最好减少底层数组的扩容次数。预估计一下,给一个大一些初始化容量。
关键点:给一个合适的初始化容量。可以提高程序的执行效率。
// 创建一个初始化容量为16个byte[] 数组。(字符串缓冲区对象)StringBuffer stringBuffer = new StringBuffer();// 拼接字符串,以后拼接字符串统一调用 append()方法。// append是追加的意思。// abd3.14true100stringBuffer.append("a");stringBuffer.append("b");stringBuffer.append("d");stringBuffer.append(3.14);stringBuffer.append(true);// append方法底层在进行追加的时候,如果byte数组满了,会自动扩容。stringBuffer.append(100L);System.out.println(stringBuffer.toString());// 指定初始化容量的StringBuffer对象(字符串缓冲区对象)StringBuffer sb = new StringBuffer(100);sb.append("hello");sb.append("world");sb.append("hello");sb.append("kitty");System.out.println(sb);
StringBuilder
tringBuffer和StringBuilder的区别?
StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
StringBuilder中的方法都没有:synchronized关键字修饰,表示StringBuilder在多线程环境下运行是不安全的。
StringBuffer是线程安全的。
StringBuilder是非线程安全的。
面试题:String为什么是不可变的?
我看过源代码,String类中有一个byte[]数组,这个byte[]数组采用了final修饰,
因为数组一旦创建长度不可变。并且被final修饰的引用一旦指向某个对象之后,不
可再指向其它对象,所以String是不可变的!
“abc” 无法变成 “abcd”
StringBuilder/StringBuffer为什么是可变的呢?
我看过源代码,StringBuffer/StringBuilder内部实际上是一个byte[]数组,
这个byte[]数组没有被final修饰,StringBuffer/StringBuilder的初始化
容量我记得应该是16,当存满之后会进行扩容,底层调用了数组拷贝的方法
System.arraycopy()…是这样扩容的。所以StringBuilder/StringBuffer
适合于使用字符串的频繁拼接操作。
包装类
8种基本数据类型对应的包装类型名是什么?
基本数据类型 包装类型
-------------------------------------
byte java.lang.Byte(父类Number)
short java.lang.Short(父类Number)
int java.lang.Integer(父类Number)
long java.lang.Long(父类Number)
float java.lang.Float(父类Number)
double java.lang.Double(父类Number)
boolean java.lang.Boolean(父类Object)
char java.lang.Character(父类Object)
八种包装类中其中6个都是数字对应的包装类,他们的父类都是Number,可以先研究一下Number中公共的方法:
Number是一个抽象类,无法实例化对象。
Number类中有这样的方法:
byte byteValue() 以 byte 形式返回指定的数值。
abstract double doubleValue()以 double 形式返回指定的数值。
abstract float floatValue()以 float 形式返回指定的数值。
abstract int intValue()以 int 形式返回指定的数值。
abstract long longValue()以 long 形式返回指定的数值。
short shortValue()以 short 形式返回指定的数值。
这些方法其实所有的数字包装类的子类都有,这些方法是负责拆箱的。
// 123这个基本数据类型,进行构造方法的包装达到了:基本数据类型向引用数据类型的转换。// 基本数据类型 -(转换为)->引用数据类型(装箱)Integer i = new Integer(123);// 将引用数据类型--(转换为)-> 基本数据类型float f = i.floatValue();System.out.println(f); //123.0// 将引用数据类型--(转换为)-> 基本数据类型(拆箱)int retValue = i.intValue();System.out.println(retValue); //123// Java9之后不建议使用这个构造方法了。出现横线表示已过时。// 将数字100转换成Integer包装类型(int --> Integer)Integer x = new Integer(100);System.out.println(x);// 将String类型的数字,转换成Integer包装类型。(String --> Integer)Integer y = new Integer("123");System.out.println(y);// double -->DoubleDouble d = new Double(1.23);System.out.println(d);// String --> DoubleDouble e = new Double("3.14");System.out.println(e);}// 通过访问包装类的常量,来获取最大值和最小值System.out.println("int的最大值:" + Integer.MAX_VALUE);
// 900是基本数据类型// x是包装类型// 基本数据类型 --(自动转换)--> 包装类型:自动装箱Integer x = 900;System.out.println(x);// x是包装类型// y是基本数据类型// 包装类型 --(自动转换)--> 基本数据类型:自动拆箱int y = x;System.out.println(y);// z是一个引用,z是一个变量,z还是保存了一个对象的内存地址。Integer z = 1000; // 等同于:Integer z = new Integer(1000);// 分析为什么这个没有报错呢?// +两边要求是基本数据类型的数字,z是包装类,不属于基本数据类型,这里会进行自动拆箱。将z转换成基本数据类型// 在java5之前你这样写肯定编译器报错。System.out.println(z + 1);Integer a = 1000; // Integer a = new Integer(1000); a是个引用,保存内存地址指向对象。Integer b = 1000; // Integer b = new Integer(1000); b是个引用,保存内存地址指向对象。// == 比较的是对象的内存地址,a和b两个引用中保存的对象内存地址不同。// == 这个运算符不会触发自动拆箱机制。(只有+ - * /等运算的时候才会。)System.out.println(a == b); //false
Integer a = 128;Integer b = 128;System.out.println(a == b); //false/*java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据不需要再new了,直接从整数型常量池当 中取出来。原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存地址是一样的。*/Integer x = 127;Integer y = 127;// == 永远判断的都是两个对象的内存地址是否相同。System.out.println(x == y); //true
// 重点方法// static int parseInt(String s)// 静态方法,传参String,返回int//网页上文本框中输入的100实际上是"100"字符串。后台数据库中要求存储100数字,此时java程序需要将"100"转换成100数字。int retValue = Integer.parseInt("123"); // String -转换-> int//int retValue = Integer.parseInt("中文"); // NumberFormatExceptionSystem.out.println(retValue + 100);
xxx.valueOf 转变为xxx的包装类
// valueOf方法作为了解//static Integer valueOf(int i)// 静态的:int-->IntegerInteger i1 = Integer.valueOf(100);System.out.println(i1);// static Integer valueOf(String s)// 静态的:String-->IntegerInteger i2 = Integer.valueOf("100");System.out.println(i2);
// String --> intint i1 = Integer.parseInt("100"); // i1是100数字System.out.println(i1 + 1); // 101// int --> StringString s2 = i1 + ""; // "100"字符串System.out.println(s2 + 1); // "1001"// int --> Integer// 自动装箱Integer x = 1000;// Integer --> int// 自动拆箱int y = x;// String --> IntegerInteger k = Integer.valueOf("123");// Integer --> StringString e = String.valueOf(k);
Date
// java.util.Date类的toString()方法已经被重写了。Date nowTime = new Date(); //Fri Jun 19 20:33:09 GMT+08:00 2020/*yyyy 年(年是4位)MM 月(月是2位)dd 日HH 时mm 分ss 秒SSS 毫秒(毫秒3位,最高999。1000毫秒代表1秒)注意:在日期格式中,除了y M d H m s S这些字符不能随便写之外,剩下的符号格式自己随意组织。*/SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");String nowTimeStr = sdf.format(nowTime);// 假设现在有一个日期字符串String,怎么转换成Date类型?// String --> DateString time = "2008-08-08 08:08:08 888";
//SimpleDateFormat sdf2 = new SimpleDateFormat("格式不能随便写,要和日期字符串格式相同");// 注意:字符串的日期格式和SimpleDateFormat对象指定的日期格式要一致。不然会出现异常:java.text.ParseExceptionSimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");Date dateTime = sdf2.parse(time);System.out.println(dateTime); //Fri Aug 08 08:08:08 CST 2008
简单总结一下System类的相关属性和方法:
- System.out 【out是System类的静态变量。】
- System.out.println() 【println()方法不是System类的,是PrintStream类的方法。】
- System.gc() 建议启动垃圾回收器
- System.currentTimeMillis() 获取自1970年1月1日到系统当前时间的总毫秒数。
- System.exit(0) 退出JVM。
// 获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。long nowTimeMillis = System.currentTimeMillis();System.out.println(nowTimeMillis); //1583377912981// 获取昨天的此时的时间。Date time2 = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24);String strTime2 = sdf.format(time2);System.out.println(strTime2); //2020-03-04 11:44:14 829
数字格式化
/*数字格式有哪些?# 代表任意数字, 代表千分位. 代表小数点0 代表不够时补0###,###.##表示:加入千分位,保留2个小数。*/DecimalFormat df = new DecimalFormat("###,###.##");String s = df.format(1234.561232);System.out.println(s); // "1,234.56"DecimalFormat df2 = new DecimalFormat("###,###.0000"); //保留4个小数位,不够补上0String s2 = df2.format(1234.56);System.out.println(s2); // "1,234.5600"
BigDecimal
BigDecimal 属于大数据,精度极高。不属于基本数据类型,属于java对象(引用数据类型)
// 这个100不是普通的100,是精度极高的100BigDecimal v1 = new BigDecimal(100);// 精度极高的200BigDecimal v2 = new BigDecimal(200);// 求和// v1 + v2; // 这样不行,v1和v2都是引用,不能直接使用+求和。BigDecimal v3 = v1.add(v2); // 调用方法求和。System.out.println(v3); //300
随机数
// 创建随机数对象Random random = new Random();// 随机产生一个int类型取值范围内的数字。int num1 = random.nextInt();System.out.println(num1);// 产生[0~100]之间的随机数。不能产生101。// nextInt翻译为:下一个int类型的数据是101,表示只能取到100.int num2 = random.nextInt(101); //不包括101System.out.println(num2);
枚举
总结:
1、枚举是一种引用数据类型
2、枚举类型怎么定义,语法是?
enum 枚举类型名{
枚举值1,枚举值2
}
3、结果只有两种情况的,建议使用布尔类型。
结果超过两种并且还是可以一枚一枚列举出来的,建议使用枚举类型。
例如:颜色、四季、星期等都可以使用枚举类型。
// 枚举:一枚一枚可以列举出来的,才建议使用枚举类型。
// 枚举编译之后也是生成class文件。
// 枚举也是一种引用数据类型。
// 枚举中的每一个值可以看做是常量。
enum Result{// SUCCESS 是枚举Result类型中的一个值// FAIL 是枚举Result类型中的一个值// 枚举中的每一个值,可以看做是“常量”SUCCESS, FAIL
}
异常
Object
|
Throwable
/ \
Error(不可处理,直接退出JVM) Exception(可处理的)
/ \
编译时异常 运行时异常
1.2、编译时异常和运行时异常,都是发生在运行阶段。编译阶段异常是不会发生的。
编译时异常因为什么而得名?
因为编译时异常必须在编译(编写)阶段预先处理,如果不处理编译器报错,因此得名。
所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象。
因为异常的发生就是new异常对象。
1.3、编译时异常和运行时异常的区别?
编译时异常一般发生的概率比较高。
运行时异常一般发生的概率比较低。
1.7、Java语言中对异常的处理包括两种方式:
第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。
谁调用我,我就抛给谁。抛给上一级。
第二种方式:使用try…catch语句进行异常的捕捉。
这件事发生了,谁也不知道,因为我给抓住了。
思考:
异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要
对这个异常继续处理,那么调用者处理这个异常同样有两种处理方式。
1.8、注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续
向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果。终止java程序的执行。
/*
以下代码报错的原因是什么?因为doSome()方法声明位置上使用了:throws ClassNotFoundException而ClassNotFoundException是编译时异常。必须编写代码时处理,没有处理编译器报错。*/
public class ExceptionTest04 {public static void main(String[] args) {// main方法中调用doSome()方法// 因为doSome()方法声明位置上有:throws ClassNotFoundException// 我们在调用doSome()方法的时候必须对这种异常进行预先的处理。// 如果不处理,编译器就报错。//编译器报错信息: Unhandled exception: java.lang.ClassNotFoundException//doSome();}/*** doSome方法在方法声明的位置上使用了:throws ClassNotFoundException* 这个代码表示doSome()方法在执行过程中,有可能会出现ClassNotFoundException异常。* 叫做类没找到异常。这个异常直接父类是:Exception,所以ClassNotFoundException属于编译时异常。* @throws ClassNotFoundException*/public static void doSome() throws ClassNotFoundException{System.out.println("doSome!!!!");}}
注意:
只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。
另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行。
try…catch捕捉异常之后,后续代码可以执行。
public class ExceptionTest06 {/*main beginm1 beginm2 begin文件不存在,可能路径错误,也可能该文件被删除了!java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)main over*/public static void main(String[] args) {System.out.println("main begin");try {// try尝试m1();// 以上代码出现异常,直接进入catch语句块中执行。System.out.println("hello world!");} catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。// 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。// catch是捕捉异常之后走的分支。// 在catch分支中干什么?处理异常。System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");System.out.println(e); //java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)}// try..catch把异常抓住之后,这里的代码会继续执行。System.out.println("main over");}private static void m1() throws FileNotFoundException {System.out.println("m1 begin");m2();// 以上代码出异常,这里是无法执行的。System.out.println("m1 over");}private static void m2() throws FileNotFoundException {System.out.println("m2 begin");// 编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException// 我们在这里调用m3()没有对异常进行预处理,所以编译报错。// m3();m3();// 以上如果出现异常,这里是无法执行的!System.out.println("m2 over");}private static void m3() throws FileNotFoundException {/*编译报错的原因是什么?第一:这里调用了一个构造方法:FileInputStream(String name)第二:这个构造方法的声明位置上有:throws FileNotFoundException第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,最终得知,FileNotFoundException是编译时异常。错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。*///new FileInputStream("D:\\course\\01-开课\\学习方法.txt");// 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。// 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。new FileInputStream("D:\\course\\01-课\\学习方法.txt");System.out.println("如果以上代码出异常,这里会执行吗??????????????????不会!!!");}
}
深入try…catch
1、catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
2、catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
3、catch写多个的时候,从上到下,必须遵守从小到大。
/*try {//创建输入流FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");//读文件fis.read();} catch(IOException e){System.out.println("读文件报错了!");} catch(FileNotFoundException e) {System.out.println("文件不存在!");}*/// JDK8的新特性!try {//创建输入流FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");// 进行数学运算System.out.println(100 / 0); // 这个异常是运行时异常,编写程序时可以处理,也可以不处理。} catch(FileNotFoundException | ArithmeticException | NullPointerException e) {System.out.println("文件不存在?数学异常?空指针异常?都有可能!");}
异常对象有两个非常重要的方法:
获取异常简单的描述信息:
String msg = exception.getMessage();
打印异常追踪的堆栈信息:
exception.printStackTrace();
// 这里只是为了测试getMessage()方法和printStackTrace()方法。// 这里只是new了异常对象,但是没有将异常对象抛出。JVM会认为这是一个普通的java对象。NullPointerException e = new NullPointerException("空指针异常fdsafdsafdsafds");// 获取异常简单描述信息:这个信息实际上就是构造方法上面String参数。String msg = e.getMessage(); //空指针异常fdsafdsafdsafdsSystem.out.println(msg);// 打印异常堆栈信息// java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的。e.printStackTrace();
关于try…catch中的finally子句:
1、在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常。
finally子句必须和try一起出现,不能单独编写。
2、finally语句通常使用在哪些情况下呢?
通常在finally语句块中完成资源的释放/关闭。
因为finally中的代码比较有保障。
即使try语句块中的代码出现异常,finally中代码也会正常执行。
/*try和finally,没有catch可以吗?可以。try不能单独使用。try finally可以联合使用。以下代码的执行顺序:先执行try...再执行finally...最后执行 return (return语句只要执行方法必然结束。)*/try {System.out.println("try...");return;} finally {// finally中的语句会执行。能执行到。System.out.println("finally...");}// 这里不能写语句,因为这个代码是无法执行到的。//System.out.println("Hello World!");
public static void main(String[] args) {int result = m();System.out.println(result); //100}/*java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):java中有一条这样的规则:方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)java中还有一条语法规则:return语句一旦执行,整个方法必须结束(亘古不变的语法!)*/public static int m(){int i = 100;try {// 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100// return语句还必须保证是最后执行的。一旦执行,整个方法结束。return i;} finally {i++;}}/*
反编译之后的效果
public static int m(){int i = 100;int j = i;i++;return j;
}*/
final finally finalize有什么区别?
final 关键字
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值。
finally 关键字
和try一起联合使用。
finally语句块中的代码是必须执行的。
finalize 标识符
是一个Object类中的方法名。
这个方法是由垃圾回收器GC负责调用的。
自定义异常
/*
1、SUN提供的JDK内置的异常肯定是不够的用的。在实际的开发中,有很多业务,
这些业务出现异常之后,JDK中都是没有的。和业务挂钩的。那么异常类我们
程序员可以自己定义吗?可以。2、Java中怎么自定义异常呢?两步:第一步:编写一个类继承Exception或者RuntimeException.第二步:提供两个构造方法,一个无参数的,一个带有String参数的。死记硬背。*/
public class MyException extends Exception{ // 编译时异常public MyException(){}public MyException(String s){super(s);}
}
总结异常中的关键字:
异常捕捉:
try
catch
finally
throws 在方法声明位置上使用,表示上报异常信息给调用者。
throw 手动抛出异常!
集合
1.2、集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,
集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用。)
list.add(100); //自动装箱Integer
注意:
集合在java中本身是一个容器,是一个对象。
集合中任何时候存储的都是“引用”。
1.6、在java中集合分为两大类:
一类是单个方式存储元素:
单个方式存储元素,这一类集合中超级父接口:java.util.Collection;
一类是以键值对儿的方式存储元素
以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map;
Collection
2、Collection中的常用方法
boolean add(Object e) 向集合中添加元素
int size() 获取集合中元素的个数
void clear() 清空集合
boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true,不包含返回false
boolean remove(Object o) 删除集合中的某个元素。
boolean isEmpty() 判断该集合中元素的个数是否为0
Object[] toArray() 调用这个方法可以把集合转换成数组。【作为了解,使用不多。】
// 创建一个集合对象//Collection c = new Collection(); // 接口是抽象的,无法实例化。// 多态Collection c = new ArrayList();// 测试Collection接口中的常用方法c.add(1200); // 自动装箱(java5的新特性。),实际上是放进去了一个对象的内存地址。Integer x = new Integer(1200);c.add(3.14); // 自动装箱c.add(new Object());c.add(new Student());c.add(true); // 自动装箱// 获取集合中元素的个数System.out.println("集合中元素个数是:" + c.size()); // 5// 清空集合c.clear();System.out.println("集合中元素个数是:" + c.size()); // 0// 再向集合中添加元素c.add("hello"); // "hello"对象的内存地址放到了集合当中。c.add("world");c.add("浩克");c.add("绿巨人");c.add(1);// 判断集合中是否包含"绿巨人"boolean flag = c.contains("绿巨人");System.out.println(flag); // trueboolean flag2 = c.contains("绿巨人2");System.out.println(flag2); // falseSystem.out.println(c.contains(1)); // trueSystem.out.println("集合中元素个数是:" + c.size()); // 5// 删除集合中某个元素c.remove(1);System.out.println("集合中元素个数是:" + c.size()); // 4// 判断集合是否为空(集合中是否存在元素)System.out.println(c.isEmpty()); // false// 清空c.clear();System.out.println(c.isEmpty()); // true(true表示集合中没有元素了!)c.add("abc");c.add("def");c.add(100);c.add("helloworld!");c.add(new Student());// 转换成数组(了解,使用不多。)Object[] objs = c.toArray();for(int i = 0; i < objs.length; i++){// 遍历数组Object o = objs[i];System.out.println(o);}
iterator
// 注意:以下讲解的遍历方式/迭代方式,是所有Collection通用的一种方式。// 在Map集合中不能用。在所有的Collection以及子类中使用。
Iterator it = c.iterator();// 第二步:通过以上获取的迭代器对象开始迭代/遍历集合。/*以下两个方法是迭代器对象Iterator中的方法:boolean hasNext()如果仍有元素可以迭代,则返回 true。Object next() 返回迭代的下一个元素。*/while(it.hasNext()){Object obj = it.next();System.out.println(obj);}
contains
深入Collection集合的contains方法:
boolean contains(Object o)
判断集合中是否包含某个对象o
如果包含返回true, 如果不包含返回false。
contains方法是用来判断集合中是否包含某个元素的方法,
那么它在底层是怎么判断集合中是否包含某个元素的呢?
调用了equals方法进行比对。
equals方法返回true,就表示包含这个元素。
// 创建集合对象Collection c = new ArrayList();// 向集合中存储元素String s1 = new String("abc"); // s1 = 0x1111c.add(s1); // 放进去了一个"abc"String s2 = new String("def"); // s2 = 0x2222c.add(s2);// 集合中元素的个数System.out.println("元素的个数是:" + c.size()); // 2// 新建的对象StringString x = new String("abc"); // x = 0x5555// c集合中是否包含x?结果猜测一下是true还是false?System.out.println(c.contains(x)); //判断集合中是否存在"abc" true
结论:存放在一个集合中的类型,一定要重写equals方法。
remove
// 创建集合对象Collection cc = new ArrayList();// 创建字符串对象String s1 = new String("hello");// 加进去。cc.add(s1);// 创建了一个新的字符串对象String s2 = new String("hello");// 删除s2cc.remove(s2); // s1.equals(s2) java认为s1和s2是一样的。删除s2就是删除s1。// 集合中元素个数是?System.out.println(cc.size()); // 0
remove
关于集合元素的remove
重点:当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器,会出现
异常:java.util.ConcurrentModificationException
重点:在迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:
c.remove(o); 迭代过程中不能这样。
会出现:java.util.ConcurrentModificationException
重点:在迭代元素的过程当中,一定要使用迭代器Iterator的remove方法,删除元素, it.remove
不要使用集合自带的remove方法删除元素。
List
1、List集合存储元素特点:有序可重复
有序:List集合中的元素有下标。
从0开始,以1递增。
可重复:存储一个1,还可以再存储1.
2、List既然是Collection接口的子接口,那么肯定List接口有自己“特色”的方法:
以下只列出List接口特有的常用的方法:
void add(int index, Object element)
Object set(int index, Object element)
Object get(int index)
int indexOf(Object o)
int lastIndexOf(Object o)
Object remove(int index)
// 创建List类型的集合。//List myList = new LinkedList();//List myList = new Vector();List myList = new ArrayList();// 添加元素myList.add("A"); // 默认都是向集合末尾添加元素。myList.add("B");myList.add("C");myList.add("C");myList.add("D");//在列表的指定位置插入指定元素(第一个参数是下标)// 这个方法使用不多,因为对于ArrayList集合来说效率比较低。myList.add(1, "KING");// 迭代Iterator it = myList.iterator();while(it.hasNext()){Object elt = it.next();System.out.println(elt);}// 根据下标获取元素Object firstObj = myList.get(0);System.out.println(firstObj);// 因为有下标,所以List集合有自己比较特殊的遍历方式// 通过下标遍历。【List集合特有的方式,Set没有。】for(int i = 0; i < myList.size(); i++){Object obj = myList.get(i);System.out.println(obj);}// 获取指定对象第一次出现处的索引。System.out.println(myList.indexOf("C")); // 3// 获取指定对象最后一次出现处的索引。System.out.println(myList.lastIndexOf("C")); // 4// 删除指定下标位置的元素// 删除下标为0的元素myList.remove(0);System.out.println(myList.size()); // 5System.out.println("====================================");// 修改指定位置的元素myList.set(2, "Soft");// 遍历集合for(int i = 0; i < myList.size(); i++){Object obj = myList.get(i);System.out.println(obj);}
ArrayList
ArrayList集合:
1、默认初始化容量10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。)
2、集合底层是一个Object[]数组。
3、构造方法:
new ArrayList();
new ArrayList(20);
4、ArrayList集合的扩容:
增长到原容量的1.5倍。
ArrayList集合底层是数组,怎么优化?
尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合
的时候预估计元素的个数,给定一个初始化容量。
5、数组优点:
检索效率比较高。(每个元素占用空间大小相同,内存地址是连续的,知道首元素内存地址,
然后知道下标,通过数学表达式计算出元素的内存地址,所以检索效率最高。)
6、数组缺点:
随机增删元素效率比较低。
另外数组无法存储大数据量。(很难找到一块非常巨大的连续的内存空间。)
7、向数组末尾添加元素,效率很高,不受影响。
8、面试官经常问的一个问题?
这么多的集合中,你用哪个集合最多?
答:ArrayList集合。
因为往数组末尾添加元素,效率不受影响。
另外,我们检索/查找某个元素的操作比较多。
9、ArrayList集合是非线程安全的。(不是线程安全的集合。)
// 指定初始化容量// 数组的长度是20List list2 = new ArrayList(20);// 集合的size()方法是获取当前集合中元素的个数。不是获取集合的容量。System.out.println(list2.size()); // 0list1.add(1);list1.add(2);list1.add(3);list1.add(4);list1.add(5);list1.add(6);list1.add(7);list1.add(8);list1.add(9);list1.add(10);System.out.println(list1.size());// 再加一个元素list1.add(11);System.out.println(list1.size()); // 11个元素。
LinkedList
链表的优点:
由于链表上的元素在空间存储上内存地址不连续。
所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。
在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议
使用LinkedList。
链表的缺点:
不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头
节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率
较低。
ArrayList:把检索发挥到极致。(末尾添加元素效率还是很高的。)
LinkedList:把随机增删发挥到极致。
加元素都是往末尾添加,所以ArrayList用的比LinkedList多。
// LinkedList集合底层也是有下标的。// 注意:ArrayList之所以检索效率比较高,不是单纯因为下标的原因。是因为底层数组发挥的作用。// LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为只能从头节点开始一个一个遍历。List list = new LinkedList();list.add("a");list.add("b");list.add("c");for(int i = 0; i <list.size(); i++){Object obj = list.get(i);System.out.println(obj);}// LinkedList集合有初始化容量吗?没有。// 最初这个链表中没有任何元素。first和last引用都是null。// 不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合。// 因为我们要面向接口编程,调用的方法都是接口中的方法。//List list2 = new ArrayList(); // 这样写表示底层你用了数组。List list2 = new LinkedList(); // 这样写表示底层你用了双向链表。// 以下这些方法你面向的都是接口编程。list2.add("123");list2.add("456");list2.add("789");for(int i = 0; i < list2.size(); i++){System.out.println(list2.get(i));}
Vector
Vector:
1、底层也是一个数组。
2、初始化容量:10
3、怎么扩容的?
扩容之后是原容量的2倍。
10–> 20 --> 40 --> 80
4、ArrayList集合扩容特点:
ArrayList集合扩容是原容量1.5倍。
5、Vector中所有的方法都是线程同步的,都带有synchronized关键字,
是线程安全的。效率比较低,使用较少了。
6、怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
使用集合工具类:
java.util.Collections;
java.util.Collection 是集合接口。
java.util.Collections 是集合工具类。
// 创建一个Vector集合List vector = new Vector();//Vector vector = new Vector();// 添加元素// 默认容量10个。vector.add(1);vector.add(2);vector.add(3);vector.add(4);vector.add(5);vector.add(6);vector.add(7);vector.add(8);vector.add(9);vector.add(10);// 满了之后扩容(扩容之后的容量是20.)vector.add(11);Iterator it = vector.iterator();while(it.hasNext()){Object obj = it.next();System.out.println(obj);}// 这个可能以后要使用!!!!List myList = new ArrayList(); // 非线程安全的。// 变成线程安全的Collections.synchronizedList(myList); // 这里没有办法看效果,因为多线程没学,你记住先!// myList集合就是线程安全的了。myList.add("111");myList.add("222");myList.add("333");}
泛型
1、JDK5.0之后推出的新特性:泛型
2、泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)
3、使用了泛型好处是什么?
第一:集合中存储的元素类型统一了。
第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”!
4、泛型的缺点是什么?
导致集合中存储的元素缺乏多样性!
大多数业务中,集合中元素的类型还是统一的。所以这种泛型特性被大家所认可。
// 使用JDK5之后的泛型机制// 使用泛型List<Animal>之后,表示List集合中只允许存储Animal类型的数据。// 用泛型来指定集合中存储的数据类型。List<Animal> myList = new ArrayList<Animal>();// 指定List集合中只能存储Animal,那么存储String就编译报错了。// 这样用了泛型之后,集合中元素的数据类型更加统一了。//myList.add("abc");Cat c = new Cat();Bird b = new Bird();myList.add(c);myList.add(b);// 获取迭代器// 这个表示迭代器迭代的是Animal类型。Iterator<Animal> it = myList.iterator();while(it.hasNext()){// 使用泛型之后,每一次迭代返回的数据都是Animal类型。//Animal a = it.next();// 这里不需要进行强制类型转换了。直接调用。//a.move();// 调用子类型特有的方法还是需要向下转换的!Animal a = it.next();if(a instanceof Cat) {Cat x = (Cat)a;x.catchMouse();}if(a instanceof Bird) {Bird y = (Bird)a;y.fly();}}
自定义泛型可以吗?可以
自定义泛型的时候,<> 尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:
和
E是Element单词首字母。
T是Type单词首字母。
foreach
// 增强for(foreach)// 以下是语法/*for(元素类型 变量名 : 数组或集合){System.out.println(变量名);}*/System.out.println("======================================");// foreach有一个缺点:没有下标。在需要使用下标的循环中,不建议使用增强for循环。for(int data : arr) {// data就是数组中的元素(数组中的每一个元素。)System.out.println(data);}
HashSet
HashSet集合:
无序不可重复。
// 演示一下HashSet集合特点Set<String> strs = new HashSet<>();// 添加元素strs.add("hello3");strs.add("hello4");strs.add("hello1");strs.add("hello2");strs.add("hello3");strs.add("hello3");strs.add("hello3");strs.add("hello3");// 遍历/*hello1hello4hello2hello31、存储时顺序和取出的顺序不同。2、不可重复。3、放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。*/for(String s : strs){System.out.println(s);}
TreeSet
TreeSet集合存储元素特点:
无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
1、TreeSet集合底层实际上是一个TreeMap
2、TreeMap集合底层是一个二叉树。
3、放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。
4、TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。称为:可排序集合。
自定义类型的排序
public class TreeSetTest04 {public static void main(String[] args) {Customer c1 = new Customer(32);Customer c2 = new Customer(20);Customer c3 = new Customer(30);Customer c4 = new Customer(25);// 创建TreeSet集合TreeSet<Customer> customers = new TreeSet<>();// 添加元素customers.add(c1);customers.add(c2);customers.add(c3);customers.add(c4);// 遍历for (Customer c : customers){System.out.println(c);}}
}// 放在TreeSet集合中的元素需要实现java.lang.Comparable接口。
// 并且实现compareTo方法。equals可以不写。
class Customer implements Comparable<Customer>{int age;public Customer(int age){this.age = age;}// 需要在这个方法中编写比较的逻辑,或者说比较的规则,按照什么进行比较!// k.compareTo(t.key)// 拿着参数k和集合中的每一个k进行比较,返回值可能是>0 <0 =0// 比较规则最终还是由程序员指定的:例如按照年龄升序。或者按照年龄降序。@Overridepublic int compareTo(Customer c) { // c1.compareTo(c2);// this是c1// c是c2// c1和c2比较的时候,就是this和c比较。/*int age1 = this.age;int age2 = c.age;if(age1 == age2){return 0;} else if(age1 > age2) {return 1;} else {return -1;}*///return this.age - c.age; // =0 >0 <0return c.age - this.age;}public String toString(){return "Customer[age="+age+"]";}
}
/*compareTo方法的返回值很重要:返回0表示相同,value会覆盖。返回>0,会继续在右子树上找。【10 - 9 = 1 ,1 > 0的说明左边这个数字比较大。所以在右子树上找。】返回<0,会继续在左子树上找。*/@Overridepublic int compareTo(Vip v) {// 写排序规则,按照什么进行比较。if(this.age == v.age){// 年龄相同时按照名字排序。// 姓名是String类型,可以直接比。调用compareTo来完成比较。return this.name.compareTo(v.name); //字符串比较} else {// 年龄不一样return this.age - v.age;}}
/*
TreeSet集合中元素可排序的第二种方式:使用比较器的方式。
最终的结论:放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:第一种:放在集合中的元素实现java.lang.Comparable接口。第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。
Comparable和Comparator怎么选择呢?当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。Comparator接口的设计符合OCP原则。*/
public class TreeSetTest06 {public static void main(String[] args) {// 创建TreeSet集合的时候,需要使用这个比较器。// TreeSet<WuGui> wuGuis = new TreeSet<>();//这样不行,没有通过构造方法传递一个比较器进去。// 给构造方法传递一个比较器。//TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator());// 大家可以使用匿名内部类的方式(这个类没有名字。直接new接口。)TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() {@Overridepublic int compare(WuGui o1, WuGui o2) {return o1.age - o2.age;}});wuGuis.add(new WuGui(1000));wuGuis.add(new WuGui(800));wuGuis.add(new WuGui(810));for(WuGui wuGui : wuGuis){System.out.println(wuGui);}}
}// 乌龟
class WuGui{int age;public WuGui(int age){this.age = age;}@Overridepublic String toString() {return "小乌龟[" +"age=" + age +']';}
}// 单独在这里编写一个比较器
// 比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。)
/*
class WuGuiComparator implements Comparator<WuGui> {@Overridepublic int compare(WuGui o1, WuGui o2) {// 指定比较规则// 按照年龄排序return o1.age - o2.age;}
}*/
- 类实现Comparable接口 add元素然后直接输出就行了
- 匿名内部类参数构造然后实现Comparator接口,实现方法
public class Person implements Comparable<Person>{private int age;public Person(){}public Person(int age) {this.age = age;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Person{" +"age=" + age +'}';}@Overridepublic int compareTo(Person o) {return this.age-o.age;}
}Set<Person> s =new TreeSet<>();s.add(new Person(12));s.add(new Person(15));s.add(new Person(13));s.add(new Person(14));for(Person a :s){System.out.println(a);}
Set<Person> p = new TreeSet<>(new Comparator<Person>() {@Overridepublic int compare(Person o1, Person o2) {return o1.getAge()-o2.getAge();}});p.add(new Person(10));p.add(new Person(150));p.add(new Person(131));p.add(new Person(104));for(Person a :p){System.out.println(a);}
Collections
public class CollectionsTest {public static void main(String[] args) {// ArrayList集合不是线程安全的。List<String> list = new ArrayList<>();// 变成线程安全的Collections.synchronizedList(list);// 排序list.add("abf");list.add("abx");list.add("abc");list.add("abe");Collections.sort(list);for(String s : list){System.out.println(s);}List<WuGui2> wuGuis = new ArrayList<>();wuGuis.add(new WuGui2(1000));wuGuis.add(new WuGui2(8000));wuGuis.add(new WuGui2(500));// 注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。Collections.sort(wuGuis);for(WuGui2 wg : wuGuis){System.out.println(wg);}// 对Set集合怎么排序呢?Set<String> set = new HashSet<>();set.add("king");set.add("kingsoft");set.add("king2");set.add("king1");// 将Set集合转换成List集合List<String> myList = new ArrayList<>(set);Collections.sort(myList);for(String s : myList) {System.out.println(s);}// 这种方式也可以排序。//Collections.sort(list集合, 比较器对象);}
}class WuGui2 implements Comparable<WuGui2>{int age;public WuGui2(int age){this.age = age;}@Overridepublic int compareTo(WuGui2 o) {return this.age - o.age;}@Overridepublic String toString() {return "WuGui2{" +"age=" + age +'}';}
}
Map
1、Map和Collection没有继承关系。
2、Map集合以key和value的方式存储数据:键值对
key和value都是引用数据类型。
key和value都是存储对象的内存地址。
key起到主导的地位,value是key的一个附属品。
3、Map接口中常用方法:
V put(K key, V value) 向Map集合中添加键值对
V get(Object key) 通过key获取value
void clear() 清空Map集合
boolean containsKey(Object key) 判断Map中是否包含某个key
boolean containsValue(Object value) 判断Map中是否包含某个value
boolean isEmpty() 判断Map集合中元素个数是否为0
V remove(Object key) 通过key删除键值对
int size() 获取Map集合中键值对的个数。
Collection values() 获取Map集合中所有的value,返回一个Collection
Set keySet() 获取Map集合所有的key(所有的键是一个set集合)
Set<Map.Entry<K,V>> entrySet()
将Map集合转换成Set集合
假设现在有一个Map集合,如下所示:
map1集合对象
key value
1 zhangsan
2 lisi
3 wangwu
4 zhaoliu
Set set = map1.entrySet();
set集合对象
1=zhangsan
【注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是 Map.Entry<K,V>】
2=lisi
【Map.Entry和String一样,都是一种类型的名字,只不过: Map.Entry是静态内部类,是Map中的静态内部类】
3=wangwu
4=zhaoliu —> 这个东西是个什么?Map.Entry
// 创建Map集合对象Map<Integer, String> map = new HashMap<>();// 向Map集合中添加键值对map.put(1, "zhangsan"); // 1在这里进行了自动装箱。map.put(2, "lisi");map.put(3, "wangwu");map.put(4, "zhaoliu");// 通过key获取valueString value = map.get(2);System.out.println(value);// 获取键值对的数量System.out.println("键值对的数量:" + map.size());// 通过key删除key-valuemap.remove(2);System.out.println("键值对的数量:" + map.size());// 判断是否包含某个key// contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。System.out.println(map.containsKey(new Integer(4))); // true// 判断是否包含某个valueSystem.out.println(map.containsValue(new String("wangwu"))); // true// 获取所有的valueCollection<String> values = map.values();// foreachfor(String s : values){System.out.println(s);}// 清空map集合map.clear();System.out.println("键值对的数量:" + map.size());// 判断是否为空System.out.println(map.isEmpty()); // true
遍历
// 第一种方式:获取所有的key,通过遍历key,来遍历valueMap<Integer, String> map = new HashMap<>();map.put(1, "zhangsan");map.put(2, "lisi");map.put(3, "wangwu");map.put(4, "zhaoliu");// 遍历Map集合// 获取所有的key,所有的key是一个Set集合Set<Integer> keys = map.keySet();// 遍历key,通过key获取value// 迭代器可以/*Iterator<Integer> it = keys.iterator();while(it.hasNext()){// 取出其中一个keyInteger key = it.next();// 通过key获取valueString value = map.get(key);System.out.println(key + "=" + value);}*/// foreach也可以for(Integer key : keys){System.out.println(key + "=" + map.get(key));}-----------------------------------------------------------------------------------// 第二种方式:Set<Map.Entry<K,V>> entrySet()// 以上这个方法是把Map集合直接全部转换成Set集合。// Set集合中元素的类型是:Map.EntrySet<Map.Entry<Integer,String>> set = map.entrySet();// 遍历Set集合,每一次取出一个Node// 迭代器/*Iterator<Map.Entry<Integer,String>> it2 = set.iterator();while(it2.hasNext()){Map.Entry<Integer,String> node = it2.next();Integer key = node.getKey();String value = node.getValue();System.out.println(key + "=" + value);}*/// foreach// 这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。// 这种方式比较适合于大数据量。for(Map.Entry<Integer,String> node : set){System.out.println(node.getKey() + "--->" + node.getValue());}
HashMap
1、HashMap集合底层是哈希表/散列表的数据结构。
2、哈希表是一个怎样的数据结构呢?
哈希表是一个数组和单向链表的结合体。
数组:在查询方面效率很高,随机增删方面效率很低。
单向链表:在随机增删方面效率较高,在查询方面效率很低。
哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点。
3、HashMap集合底层的源代码:
public class HashMap{
// HashMap底层实际上就是一个数组。(一维数组)
Node<K,V>[] table;
// 静态的内部类HashMap.Node
static class Node<K,V> {
final int hash; // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
final K key; // 存储到Map集合中的那个key
V value; // 存储到Map集合中的那个value
Node<K,V> next; // 下一个节点的内存地址。
}
}
哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)
4、最主要掌握的是:
map.put(k,v)
v = map.get(k)
以上这两个方法的实现原理,是必须掌握的。
5、HashMap集合的key部分特点:
无序,不可重复。
为什么无序? 因为不一定挂到哪个单向链表上。
不可重复是怎么保证的? equals方法来保证HashMap集合的key不可重复。
如果key重复了,value会覆盖。
放在HashMap集合key部分的元素其实就是放到HashSet集合中了。
所以HashSet集合中的元素也需要同时重写hashCode()+equals()方法。
6、哈希表HashMap使用不当时无法发挥性能!
假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了
纯单向链表。这种情况我们成为:散列分布不均匀。
什么是散列分布均匀?
假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,
是散列分布均匀的。
假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗,有什么问题?
不行,因为这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。
也是散列分布不均匀。
散列分布均匀需要你重写hashCode()方法时有一定的技巧。
7、重点:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。
8、HashMap集合的默认初始化容量是16,默认加载因子是0.75
这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。
重点,记住:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,
这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。
public static void main(String[] args) {// 测试HashMap集合key部分的元素特点// Integer是key,它的hashCode和equals都重写了。Map<Integer,String> map = new HashMap<>();map.put(1111, "zhangsan");map.put(6666, "lisi");map.put(7777, "wangwu");map.put(2222, "zhaoliu");map.put(2222, "king"); //key重复的时候value会自动覆盖。System.out.println(map.size()); // 4// 遍历Map集合Set<Map.Entry<Integer,String>> set = map.entrySet();for(Map.Entry<Integer,String> entry : set){// 验证结果:HashMap集合key部分元素:无序不可重复。System.out.println(entry.getKey() + "=" + entry.getValue());}}
1、向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!
equals方法有可能调用,也有可能不调用。
拿put(k,v)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,
哈希值经过哈希算法转换成数组下标。
数组下标位置上如果是null,equals不需要执行。
拿get(k)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,
哈希值经过哈希算法转换成数组下标。
数组下标位置上如果是null,equals不需要执行。
2、注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写。
并且equals方法返回如果是true,hashCode()方法返回的值必须一样。
equals方法返回true表示两个对象相同,在同一个单向链表上比较。
那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。
所以hashCode()方法的返回值也应该相同。
3、hashCode()方法和equals()方法不用研究了,直接使用IDEA工具生成,但是这两个方法需要同时生成。
4、终极结论:
放在HashMap集合key部分的,以及放在HashSet集合中的元素,需要同时重写hashCode方法和equals方法。
5、对于哈希表数据结构来说:
如果o1和o2的hash值相同,一定是放到同一个单向链表上。
当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。
HashMap集合key部分允许null吗?
允许
但是要注意:HashMap集合的key null值只能有一个。
有可能面试的时候遇到这样的问题。
HashTable
Hashtable的key可以为null吗?
Hashtable的key和value都是不能为null的。
HashMap集合的key和value都是可以为null的。
Hashtable方法都带有synchronized:线程安全的。
线程安全有其它的方案,这个Hashtable对线程的处理
导致效率较低,使用较少了。
Hashtable和HashMap一样,底层都是哈希表数据结构。
Hashtable的初始化容量是11,默认加载因子是:0.75f
Hashtable的扩容是:原容量 * 2 + 1
Properties
目前只需要掌握Properties属性类对象的相关方法即可。
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。
// 创建一个Properties对象Properties pro = new Properties();// 需要掌握Properties的两个方法,一个存,一个取。pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode");pro.setProperty("driver","com.mysql.jdbc.Driver");pro.setProperty("username", "root");pro.setProperty("password", "123");// 通过key获取valueString url = pro.getProperty("url");String driver = pro.getProperty("driver");String username = pro.getProperty("username");String password = pro.getProperty("password");System.out.println(url);System.out.println(driver);System.out.println(username);System.out.println(password);
IO
一种方式是按照流的方向进行分类:
以内存作为参照物,
往内存中去,叫做输入(Input)。或者叫做读(Read)。
从内存中出来,叫做输出(Output)。或者叫做写(Write)。
另一种方式是按照读取数据方式不同进行分类:有的流是按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件等....假设文件file1.txt,采用字节流的话是这样读的:a中国bc张三fe第一次读:一个字节,正好读到'a'第二次读:一个字节,正好读到'中'字符的一半。第三次读:一个字节,正好读到'中'字符的另外一半。有的流是按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件。只能读取纯文本文件,连word文件都无法读取。假设文件file1.txt,采用字符流的话是这样读的:a中国bc张三fe第一次读:'a'字符('a'字符在windows系统中占用1个字节。)第二次读:'中'字符('中'字符在windows系统中占用2个字节。)
综上所述:流的分类
输入流、输出流
字节流、字符流
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
四大家族的首领都是抽象类。(abstract class)
所有的流都实现了:
java.io.Closeable接口,都是可关闭的,都有close()方法。
流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,
不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭。
所有的输出流都实现了:
java.io.Flushable接口,都是可刷新的,都有flush()方法。
养成一个好习惯,输出流在最终输出之后,一定要记得flush()
刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据
强行输出完(清空管道!)刷新的作用就是清空管道。
注意:如果没有flush()可能会导致丢失数据。
java.io包下需要掌握的流有16个:文件专属:java.io.FileInputStream(掌握)java.io.FileOutputStream(掌握)java.io.FileReaderjava.io.FileWriter转换流:(将字节流转换成字符流)java.io.InputStreamReaderjava.io.OutputStreamWriter缓冲流专属:java.io.BufferedReaderjava.io.BufferedWriterjava.io.BufferedInputStreamjava.io.BufferedOutputStream数据流专属:java.io.DataInputStreamjava.io.DataOutputStream标准输出流:java.io.PrintWriterjava.io.PrintStream(掌握)对象专属流:java.io.ObjectInputStream(掌握)java.io.ObjectOutputStream(掌握)
FileInputStream
/*
java.io.FileInputStream:1、文件字节输入流,万能的,任何类型的文件都可以采用这个流来读。2、字节的方式,完成输入的操作,完成读的操作(硬盘---> 内存)*/
public class FileInputStreamTest01 {public static void main(String[] args) {FileInputStream fis = null;try {// 创建文件字节输入流对象// 文件路径:D:\course\JavaProjects\02-JavaSE\temp (IDEA会自动把\编程\\,因为java中\表示转义)// 以下都是采用了:绝对路径的方式。//FileInputStream fis = new FileInputStream("D:\\course\\JavaProjects\\02-JavaSE\\temp");// 写成这个/也是可以的。fis = new FileInputStream("D:/course/JavaProjects/02-JavaSE/temp");// 开始读int readData = fis.read(); // 这个方法的返回值是:读取到的“字节”本身。System.out.println(readData); //97readData = fis.read();System.out.println(readData); //98readData = fis.read();System.out.println(readData); //99readData = fis.read();System.out.println(readData); //100readData = fis.read();System.out.println(readData); //101readData = fis.read();System.out.println(readData); //102// 已经读到文件的末尾了,再读的时候读取不到任何数据,返回-1.readData = fis.read();System.out.println(readData);readData = fis.read();System.out.println(readData);readData = fis.read();System.out.println(readData);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {// 在finally语句块当中确保流一定关闭。if (fis != null) { // 避免空指针异常!// 关闭流的前提是:流不是空。流是null的时候没必要关闭。try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}
升级
分析这个程序的缺点:
一次读取一个字节byte,这样内存和硬盘交互太频繁,基本上时间/资源都耗费
在交互上面了。能不能一次读取多个字节呢?可以。
int readData = 0;while((readData = fis.read()) != -1){System.out.println(readData);}
最终版,需要掌握。
public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("chapter23/src/tempfile3");// 准备一个byte数组byte[] bytes = new byte[4];/*while(true){int readCount = fis.read(bytes);if(readCount == -1){break;}// 把byte数组转换成字符串,读到多少个转换多少个。System.out.print(new String(bytes, 0, readCount));}*/int readCount = 0;while((readCount = fis.read(bytes)) != -1) {System.out.print(new String(bytes, 0, readCount));}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
FileInputStream类的其它常用方法:
int available():返回流当中剩余的没有读到的字节数量
long skip(long n):跳过几个字节不读。
fis = new FileInputStream("tempfile");System.out.println("总字节数量:" + fis.available());// 读1个字节//int readByte = fis.read();// 还剩下可以读的字节数量是:5//System.out.println("剩下多少个字节没有读:" + fis.available());// 这个方法有什么用?//byte[] bytes = new byte[fis.available()]; // 这种方式不太适合太大的文件,因为byte[]数组不能太大。// 不需要循环了。// 直接读一次就行了。//int readCount = fis.read(bytes); // 6//System.out.println(new String(bytes)); // abcdef// skip跳过几个字节不读取,这个方法也可能以后会用!fis.skip(3);System.out.println(fis.read()); //100
FileOutputStream
FileOutputStream fos = null;try {// myfile文件不存在的时候会自动新建!// 这种方式谨慎使用,这种方式会先将原文件清空,然后重新写入。//fos = new FileOutputStream("myfile");//fos = new FileOutputStream("chapter23/src/tempfile3");// 以追加的方式在文件末尾写入。不会清空原文件内容。fos = new FileOutputStream("chapter23/src/tempfile3", true);// 开始写。byte[] bytes = {97, 98, 99, 100};// 将byte数组全部写出!fos.write(bytes); // abcd// 将byte数组的一部分写出!fos.write(bytes, 0, 2); // 再写出ab// 字符串String s = "我是一个中国人,我骄傲!!!";// 将字符串转换成byte数组。byte[] bs = s.getBytes();// 写fos.write(bs);// 写完之后,最后一定要刷新fos.flush();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}
copy
使用FileInputStream + FileOutputStream完成文件的拷贝。
拷贝的过程应该是一边读,一边写。
使用以上的字节流拷贝文件的时候,文件类型随意,万能的。什么样的文件都能拷贝。
FileInputStream fis = null;FileOutputStream fos = null;try {// 创建一个输入流对象fis = new FileInputStream("D:\\course\\02-JavaSE\\video\\chapter01\\动力节点-JavaSE-杜聚宾-001-文件扩展名的显示.avi");// 创建一个输出流对象fos = new FileOutputStream("C:\\动力节点-JavaSE-杜聚宾-001-文件扩展名的显示.avi");// 最核心的:一边读,一边写byte[] bytes = new byte[1024 * 1024]; // 1MB(一次最多拷贝1MB。)int readCount = 0;while((readCount = fis.read(bytes)) != -1) {fos.write(bytes, 0, readCount);}// 刷新,输出流最后要刷新fos.flush();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {// 分开try,不要一起try。// 一起try的时候,其中一个出现异常,可能会影响到另一个流的关闭。if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}
使用FileReader FileWriter进行拷贝的话,只能拷贝“普通文本”文件。
FileReader in = null;FileWriter out = null;try {// 读in = new FileReader("chapter23/src/com/bjpowernode/java/io/Copy02.java");// 写out = new FileWriter("Copy02.java");// 一边读一边写:char[] chars = new char[1024 * 512]; // 1MBint readCount = 0;while((readCount = in.read(chars)) != -1){out.write(chars, 0, readCount);}// 刷新out.flush();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}if (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}}
FileReader
FileReader reader = null;try {// 创建文件字符输入流reader = new FileReader("tempfile");//准备一个char数组char[] chars = new char[4];// 往char数组中读reader.read(chars); // 按照字符的方式读取:第一次e,第二次f,第三次 风....for(char c : chars) {System.out.println(c);}/*// 开始读char[] chars = new char[4]; // 一次读取4个字符int readCount = 0;while((readCount = reader.read(chars)) != -1) {System.out.print(new String(chars,0,readCount));}*/} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (reader != null) {try {reader.close();} catch (IOException e) {e.printStackTrace();}}}
FileWriter
FileWriter out = null;try {// 创建文件字符输出流对象//out = new FileWriter("file");out = new FileWriter("file", true);// 开始写。char[] chars = {'我','是','中','国','人'};out.write(chars);out.write(chars, 2, 3);out.write("我是一名java软件工程师!");// 写出一个换行符。out.write("\n");out.write("hello world!");// 刷新out.flush();} catch (IOException e) {e.printStackTrace();} finally {if (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}
BufferedReader
带有缓冲区的字符输入流。
使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组。自带缓冲。
FileReader reader = new FileReader("Copy02.java");// 当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流。// 外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流。// 像当前这个程序来说:FileReader就是一个节点流。BufferedReader就是包装流/处理流。BufferedReader br = new BufferedReader(reader);// 读一行/*String firstLine = br.readLine();System.out.println(firstLine);String secondLine = br.readLine();System.out.println(secondLine);String line3 = br.readLine();System.out.println(line3);*/// br.readLine()方法读取一个文本行,但不带换行符。String s = null;while((s = br.readLine()) != null){System.out.print(s);}// 关闭流// 对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭。(可以看源代码。)br.close();
InputStreamReader
/*// 字节流FileInputStream in = new FileInputStream("Copy02.java");// 通过转换流转换(InputStreamReader将字节流转换成字符流。)// in是节点流。reader是包装流。InputStreamReader reader = new InputStreamReader(in);// 这个构造方法只能传一个字符流。不能传字节流。// reader是节点流。br是包装流。BufferedReader br = new BufferedReader(reader);*/// 合并BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("Copy02.java")));String line = null;while((line = br.readLine()) != null){System.out.println(line);}// 关闭最外层br.close();}
/*
BufferedWriter:带有缓冲的字符输出流。
OutputStreamWriter:转换流*/
public class BufferedWriterTest {public static void main(String[] args) throws Exception{// 带有缓冲区的字符输出流//BufferedWriter out = new BufferedWriter(new FileWriter("copy"));BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy", true)));// 开始写。out.write("hello world!");out.write("\n");out.write("hello kitty!");// 刷新out.flush();// 关闭最外层out.close();}
}
DataInputStream:数据字节输入流。
DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候你需要提前知道写入的顺序。
读的顺序需要和写的顺序一致。才可以正常取出数据。
DataInputStream dis = new DataInputStream(new FileInputStream("data"));// 开始读byte b = dis.readByte();short s = dis.readShort();int i = dis.readInt();long l = dis.readLong();float f = dis.readFloat();double d = dis.readDouble();boolean sex = dis.readBoolean();char c = dis.readChar();System.out.println(b);System.out.println(s);System.out.println(i + 1000);System.out.println(l);System.out.println(f);System.out.println(d);System.out.println(sex);System.out.println(c);dis.close();
// 创建数据专属的字节输出流DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));// 写数据byte b = 100;short s = 200;int i = 300;long l = 400L;float f = 3.0F;double d = 3.14;boolean sex = false;char c = 'a';// 写dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。dos.writeShort(s);dos.writeInt(i);dos.writeLong(l);dos.writeFloat(f);dos.writeDouble(d);dos.writeBoolean(sex);dos.writeChar(c);// 刷新dos.flush();// 关闭最外层dos.close();
}
PrintStream
标准的字节输出流。默认输出到控制台。
// 联合起来写System.out.println("hello world!");// 分开写PrintStream ps = System.out;ps.println("hello zhangsan");ps.println("hello lisi");ps.println("hello wangwu");// 标准输出流不需要手动close()关闭。// 可以改变标准输出流的输出方向吗? 可以/*// 这些是之前System类使用过的方法和属性。System.gc();System.currentTimeMillis();PrintStream ps2 = System.out;System.exit(0);System.arraycopy(....);*/// 标准输出流不再指向控制台,指向“log”文件。PrintStream printStream = new PrintStream(new FileOutputStream("log"));// 修改输出方向,将输出方向修改到"log"文件。System.setOut(printStream);// 再输出System.out.println("hello world");System.out.println("hello kitty");System.out.println("hello zhangsan");
File
// 创建一个File对象File f1 = new File("D:\\file");// 判断是否存在!System.out.println(f1.exists());// 如果D:\file不存在,则以文件的形式创建出来/*if(!f1.exists()) {// 以文件形式新建f1.createNewFile();}*/// 如果D:\file不存在,则以目录的形式创建出来/*if(!f1.exists()) {// 以目录的形式新建。f1.mkdir();}*/// 可以创建多重目录吗?File f2 = new File("D:/a/b/c/d/e/f");/*if(!f2.exists()) {// 多重目录的形式新建。f2.mkdirs();}*/File f3 = new File("D:\\course\\01-开课\\学习方法.txt");// 获取文件的父路径String parentPath = f3.getParent();System.out.println(parentPath); //D:\course\01-开课File parentFile = f3.getParentFile();System.out.println("获取绝对路径:" + parentFile.getAbsolutePath());File f4 = new File("copy");System.out.println("绝对路径:" + f4.getAbsolutePath()); // C:\Users\Administrator\IdeaProjects\javase\copyFile f1 = new File("D:\\course\\01-开课\\开学典礼.ppt");// 获取文件名System.out.println("文件名:" + f1.getName());// 判断是否是一个目录System.out.println(f1.isDirectory()); // false// 判断是否是一个文件System.out.println(f1.isFile()); // true// 获取文件最后一次修改时间long haoMiao = f1.lastModified(); // 这个毫秒是从1970年到现在的总毫秒数。// 将总毫秒数转换成日期?????Date time = new Date(haoMiao);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");String strTime = sdf.format(time);System.out.println(strTime);// 获取文件大小System.out.println(f1.length()); //216064字节。// File[] listFiles()// 获取当前目录下所有的子文件。File f = new File("D:\\course\\01-开课");File[] files = f.listFiles();// foreachfor(File file : files){//System.out.println(file.getAbsolutePath());System.out.println(file.getName());}}
序列反序列化
1、java.io.NotSerializableException : Student对象不支持序列化!!!!
2、参与序列化和反序列化的对象,必须实现Serializable接口。
3、注意:通过源代码发现,Serializable接口只是一个标志接口:
public interface Serializable {
}
这个接口当中什么代码都没有。
那么它起到一个什么作用呢?
起到标识的作用,标志的作用,java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇。
Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成
一个序列化版本号。
4、序列化版本号有什么用呢?
java.io.InvalidClassException:
com.bjpowernode.java.bean.Student;
local class incompatible:
stream classdesc serialVersionUID = -684255398724514298(十年后),
local class serialVersionUID = -3463447116624555755(十年前)
java语言中是采用什么机制来区分类的?
第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类。
第二:如果类名一样,再怎么进行类的区别?靠序列化版本号进行区分。
小鹏编写了一个类:com.bjpowernode.java.bean.Student implements Serializable
胡浪编写了一个类:com.bjpowernode.java.bean.Student implements Serializable
不同的人编写了同一个类,但“这两个类确实不是同一个类”。这个时候序列化版本就起上作用了。
对于java虚拟机来说,java虚拟机是可以区分开这两个类的,因为这两个类都实现了Serializable接口,
都有默认的序列化版本号,他们的序列化版本号不一样。所以区分开了。(这是自动生成序列化版本号的好处)
请思考?
这种自动生成序列化版本号有什么缺陷?
这种自动生成的序列化版本号缺点是:一旦代码确定之后,不能进行后续的修改,
因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java
虚拟机会认为这是一个全新的类。(这样就不好了!)
最终结论:
凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号。
这样,以后这个类即使代码修改了,但是版本号不变,java虚拟机会认为是同一个类。
/*
一次序列化多个对象呢?可以,可以将对象放到集合当中,序列化集合。
提示:参与序列化的ArrayList集合以及集合中的元素User都需要实现 java.io.Serializable接口。*/
List<User> userList = new ArrayList<>();userList.add(new User(1,"zhangsan"));userList.add(new User(2, "lisi"));userList.add(new User(3, "wangwu"));ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users"));// 序列化一个集合,这个集合对象中放了很多其他对象。oos.writeObject(userList);oos.flush();oos.close();ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users"));//Object obj = ois.readObject();//System.out.println(obj instanceof List);List<User> userList = (List<User>)ois.readObject();for(User user : userList){System.out.println(user);}ois.close();
多线程
4.1、什么是进程?什么是线程?
进程是一个应用程序(1个进程是一个软件)线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。
注意:
进程A和进程B的内存独立不共享。
线程A和线程B呢?
在java语言中:
线程A和线程B,堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。
4.4、思考一个问题:
使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束。
main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在
压栈弹栈。
4.5、分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。
单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
同时在做!!!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。
5、java语言中,实现线程有两种方式,那两种方式呢?
java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。
第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。// 定义线程类public class MyThread extends Thread{public void run(){}}// 创建线程对象MyThread t = new MyThread();// 启动线程。t.start();
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。// 定义一个可运行的类public class MyRunnable implements Runnable {public void run(){}}// 创建线程对象Thread t = new Thread(new MyRunnable());// 启动线程t.start();
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承
其它的类,更灵活。
public static void main(String[] args) {// 这里是main方法,这里的代码属于主线程,在主栈中运行。// 新建一个分支线程对象MyThread t = new MyThread();// 启动线程//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)// start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。// 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。// 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。// run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。t.start();// 这里的代码还是运行在主线程中。for(int i = 0; i < 1000; i++){System.out.println("主线程--->" + i);class MyThread extends Thread {@Overridepublic void run() {// 编写程序,这段程序运行在分支线程中(分支栈)。for(int i = 0; i < 1000; i++){System.out.println("分支线程--->" + i);}}
}
public class ThreadTest03 {public static void main(String[] args) {// 创建一个可运行的对象//MyRunnable r = new MyRunnable();// 将可运行的对象封装成一个线程对象//Thread t = new Thread(r);Thread t = new Thread(new MyRunnable()); // 合并代码// 启动线程t.start();for(int i = 0; i < 100; i++){System.out.println("主线程--->" + i);}}
}// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println("分支线程--->" + i);}}
}
匿名内部类
public class ThreadTest04 {public static void main(String[] args) {// 创建线程对象,采用匿名内部类方式。// 这是通过一个没有名字的类,new出来的对象。Thread t = new Thread(new Runnable(){@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println("t线程---> " + i);}}});// 启动线程t.start();for(int i = 0; i < 100; i++){System.out.println("main线程---> " + i);}}
}
6、关于线程对象的生命周期?
新建状态
就绪状态
运行状态
阻塞状态
死亡状态
1、怎么获取当前线程对象?
Thread t = Thread.currentThread();
返回值t就是当前线程。
2、获取线程对象的名字
String name = 线程对象.getName();
3、修改线程对象的名字
线程对象.setName(“线程名字”);
4、当线程没有设置名字的时候,默认的名字有什么规律?(了解一下)
Thread-0
Thread-1
Thread-2
Thread-3
…
class MyThread2 extends Thread {public void run(){for(int i = 0; i < 100; i++){// currentThread就是当前线程对象。当前线程是谁呢?// 当t1线程执行run方法,那么这个当前线程就是t1// 当t2线程执行run方法,那么这个当前线程就是t2Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName() + "-->" + i);//System.out.println(super.getName() + "-->" + i);//System.out.println(this.getName() + "-->" + i);}}
}// 创建线程对象MyThread2 t = new MyThread2();// 设置线程的名字t.setName("t1");// 获取线程的名字String tName = t.getName();System.out.println(tName); //Thread-0
关于线程的sleep方法:
static void sleep(long millis)
1、静态方法:Thread.sleep(1000);
2、参数是毫秒
3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
4、Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
// 让当前线程进入休眠,睡眠5秒// 当前线程是主线程!!!/*try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}*/// 5秒之后执行这里的代码//System.out.println("hello world!");for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);// 睡眠1秒try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
public class ThreadTest07 {public static void main(String[] args) {// 创建线程对象Thread t = new MyThread3();t.setName("t");t.start();// 调用sleep方法try {// 问题:这行代码会让线程t进入休眠状态吗?t.sleep(1000 * 5); // 在执行的时候还是会转换成:Thread.sleep(1000 * 5);// 这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。// 这样代码出现在main方法中,main线程睡眠。} catch (InterruptedException e) {e.printStackTrace();}// 5秒之后这里才会执行。System.out.println("hello World!");}
}class MyThread3 extends Thread {public void run(){for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}
sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程??
注意:这个不是终断线程的执行,是终止线程的睡眠。
public class ThreadTest08 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable2());t.setName("t");t.start();// 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)t.interrupt(); // 干扰,一盆冷水过去!}
}class MyRunnable2 implements Runnable {// 重点:run()当中的异常不能throws,只能try catch// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "---> begin");try {// 睡眠1年Thread.sleep(1000 * 60 * 60 * 24 * 365);} catch (InterruptedException e) {// 打印异常信息//e.printStackTrace();}//1年之后才会执行这里System.out.println(Thread.currentThread().getName() + "---> end");// 调用doOther//doOther();}// 其它方法可以throws/*public void doOther() throws Exception{}*/
}
在java中怎么强行终止一个线程的执行。
这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,
线程没有保存的数据将会丢失。不建议使用。
t.stop(); // 已过时(不建议使用。)
怎么合理的终止一个线程的执行。这种方式是很常用的。
public class ThreadTest10 {public static void main(String[] args) {MyRunable4 r = new MyRunable4();Thread t = new Thread(r);t.setName("t");t.start();// 模拟5秒try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}// 终止线程// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。r.run = false;}
}class MyRunable4 implements Runnable {// 打一个布尔标记boolean run = true;@Overridepublic void run() {for (int i = 0; i < 10; i++){if(run){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}else{// return就结束了,你在结束之前还有什么没保存的。// 在这里可以保存呀。//save....//终止当前线程return;}}}
}
实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级
最低优先级1
默认优先级是5
最高优先级10
优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)
静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。
实例方法:
void join()
合并线程
class MyThread1 extends Thread {public void doSome(){MyThread2 t = new MyThread2();t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。}}class MyThread2 extends Thread{}
@Overridepublic void run() {for(int i = 1; i <= 10000; i++) {//每100个让位一次。if(i % 100 == 0){Thread.yield(); // 当前线程暂停一下,让给主线程。}System.out.println(Thread.currentThread().getName() + "--->" + i);}}
数据安全
2.2、什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。
3、Java中有三大变量?
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈。)局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。静态变量在方法区中,方法区只有1个。堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。成员变量:可能会有线程安全问题。
synchronized代码块
/*
银行账户使用线程同步机制,解决线程安全问题。*/
public class Account {// 账号private String actno;// 余额private double balance; //实例变量。//对象Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)public Account() {}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//取款的方法public void withdraw(double money){//int i = 100;//i = 101;// 以下这几行代码必须是线程排队的,不能并发。// 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。/*线程同步机制的语法是:synchronized(){// 线程同步代码块。}synchronized后面小括号中传的这个“数据”是相当关键的。这个数据必须是多线程共享的数据。才能达到多线程排队。()中写什么?那要看你想让哪些线程同步。假设t1、t2、t3、t4、t5,有5个线程,你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?你一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。这里的共享对象是:账户对象。账户对象是共享的,那么this就是账户对象吧!!!不一定是this,这里只要是多线程共享的那个对象就行。在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)100个对象,100把锁。1个对象1把锁。以下代码的执行原理?1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。*///Object obj2 = new Object();//synchronized (this){ //如果放的account 不同 那就不行//synchronized (obj) { //实例变量只有一个//synchronized ("abc") { // "abc"在字符串常量池当中。全部都锁//synchronized (null) { // 报错:空指针。//synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。double before = this.getBalance();double after = before - money;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);//}}
}
public class AccountThread extends Thread {// 两个线程必须共享同一个账户对象。private Account act;// 通过构造方法传递过来账户对象public AccountThread(Account act) {this.act = act;}public void run(){// run方法的执行表示取款操作。// 假设取款5000double money = 5000;// 取款// 多线程并发执行这个方法。//synchronized (this) { //这里的this是AccountThread对象,这个对象不共享!synchronized (act) { // 这种方式也可以,只不过扩大了同步的范围,效率更低了。act.withdraw(money);}System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());}
}
4、如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder。
StringBuffer效率比较低。
ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的.
5、总结:synchronized有三种写法:第一种:同步代码块灵活synchronized(线程共享对象){同步代码块;}第二种:在实例方法上使用synchronized表示共享对象一定是this并且同步代码块是整个方法体。第三种:在静态方法上使用synchronized表示找类锁。类锁永远只有1把。就算创建了100个对象,那类锁也只有一把。对象锁:1个对象1把锁,100个对象100把锁。类锁:100个对象,也可能只是1把类锁。
面试题
// 面试题:
oSome方法的结束吗?
//不需要,因为doOther()方法没有synchronized
public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc = new MyClass();Thread t1 = new MyThread(mc);Thread t2 = new MyThread(mc);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}
}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}
}class MyClass {public synchronized void doSome(){System.out.println("doSome begin"); //doSome begin//doOther begin//doOther over//doSome Over/*两个方法都synchronized的话就doSome begindoSome overdoOther begindoOther Over*/try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}
}
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?//不需要,因为MyClass对象是两个,两把锁。
public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new MyThread(mc1);Thread t2 = new MyThread(mc2);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}
}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}
}class MyClass {// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?//需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。//public synchronized static void doSome(){public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}// public synchronized static void doOther(){public synchronized void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}
}
死锁
public class DeadLock {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();// t1和t2两个线程共享o1,o2Thread t1 = new MyThread1(o1,o2);Thread t2 = new MyThread2(o1,o2);t1.start();t2.start();}
}class MyThread1 extends Thread{Object o1;Object o2;public MyThread1(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){}}}
}class MyThread2 extends Thread {Object o1;Object o2;public MyThread2(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){}}}
}
6、聊一聊,我们以后开发中应该怎么解决线程安全问题?是一上来就选择线程同步吗?synchronized不是,synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。第一种方案:尽量使用局部变量代替“实例变量和静态变量”。第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制
守护线程
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
// 启动线程之前,将线程设置为守护线程t.setDaemon(true);
1.2、定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
/*
使用定时器指定定时任务。*/
public class TimerTest {public static void main(String[] args) throws Exception {// 创建定时器对象Timer timer = new Timer();//Timer timer = new Timer(true); //守护线程的方式// 指定定时任务//timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date firstTime = sdf.parse("2020-03-14 09:34:30");//timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);// 每年执行一次。//timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);//匿名内部类方式timer.schedule(new TimerTask(){@Overridepublic void run() {// code....}} , firstTime, 1000 * 10);}
}// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {@Overridepublic void run() {// 编写你需要执行的任务就行了。SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String strTime = sdf.format(new Date());System.out.println(strTime + ":成功完成了一次数据备份!");}
}
实现多线程的第三个方式
/*
实现线程的第三种方式:实现Callable接口这种方式的优点:可以获取到线程的执行结果。这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。*/
public class ThreadTest15 {public static void main(String[] args) throws Exception {// 第一步:创建一个“未来任务类”对象。// 参数非常重要,需要给一个Callable接口实现类对象。FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值// 线程执行一个任务,执行之后可能会有一个执行结果// 模拟执行System.out.println("call method begin");Thread.sleep(1000 * 10);System.out.println("call method end!");int a = 100;int b = 200;return a + b; //自动装箱(300结果变成Integer)}});// 创建线程对象Thread t = new Thread(task);// 启动线程t.start();// 这里是main方法,这是在主线程中。// 在主线程中,怎么获取t线程的返回结果?// get()方法的执行会导致“当前线程阻塞”Object obj = task.get();System.out.println("线程执行结果:" + obj);// main方法这里的程序要想执行必须等待get()方法的结束// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果// 另一个线程执行是需要时间的。System.out.println("hello world!");}
}
消费者和生产者
/*
1、使用wait方法和notify方法实现“生产者和消费者模式”2、什么是“生产者和消费者模式”?生产线程负责生产,消费线程负责消费。生产线程和消费线程要达到均衡。这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。3、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。4、wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。5、wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。7、模拟这样一个需求:仓库我们采用List集合。List集合中假设只能存储1个元素。1个元素就表示仓库满了。如果List集合中元素个数是0,就表示仓库空了。保证List集合中永远都是最多存储1个元素。必须做到这种效果:生产1个消费1个。*/
public class ThreadTest16 {public static void main(String[] args) {// 创建1个仓库对象,共享的。List list = new ArrayList();// 创建两个线程对象// 生产者线程Thread t1 = new Thread(new Producer(list));// 消费者线程Thread t2 = new Thread(new Consumer(list));t1.setName("生产者线程");t2.setName("消费者线程");t1.start();t2.start();}
}// 生产线程
class Producer implements Runnable {// 仓库private List list;public Producer(List list) {this.list = list;}@Overridepublic void run() {// 一直生产(使用死循环来模拟一直生产)while(true){// 给仓库对象list加锁。synchronized (list){if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。try {// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到这里说明仓库是空的,可以生产Object obj = new Object();list.add(obj);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒消费者进行消费list.notifyAll();}}}
}// 消费线程
class Consumer implements Runnable {// 仓库private List list;public Consumer(List list) {this.list = list;}@Overridepublic void run() {// 一直消费while(true){synchronized (list) {if(list.size() == 0){try {// 仓库已经空了。// 消费者线程等待,释放掉list集合的锁list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到此处说明仓库中有数据,进行消费。Object obj = list.remove(0);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒生产者生产。list.notifyAll();}}}
}
反射
2.1、反射机制有什么用?
通过java语言中的反射机制可以操作字节码文件。
优点类似于黑客。(可以读和修改字节码文件。)
通过反射机制可以操作代码片段。(class文件。)
2.3、反射机制相关的重要的类有哪些?
java.lang.Class:代表整个字节码,代表一个类型,代表整个类。
java.lang.reflect.Method:代表字节码中的方法字节码。代表类中的方法。
java.lang.reflect.Constructor:代表字节码中的构造方法字节码。代表类中的构造方法
java.lang.reflect.Field:代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。
java.lang.Class:public class User{// Fieldint no;// Constructorpublic User(){}public User(int no){this.no = no;}// Methodpublic void setNo(int no){this.no = no;}public int getNo(){return no;}}
/*
要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例?三种方式第一种:Class c = Class.forName("完整类名带包名");第二种:Class c = 对象.getClass();第三种:Class c = 任何类型.class;*/
public class ReflectTest01 {public static void main(String[] args) {/*Class.forName()1、静态方法2、方法的参数是一个字符串。3、字符串需要的是一个完整类名。4、完整类名必须带有包名。java.lang包也不能省略。*/Class c1 = null;Class c2 = null;try {c1 = Class.forName("java.lang.String"); // c1代表String.class文件,或者说c1代表String类型。c2 = Class.forName("java.util.Date"); // c2代表Date类型Class c3 = Class.forName("java.lang.Integer"); // c3代表Integer类型Class c4 = Class.forName("java.lang.System"); // c4代表System类型} catch (ClassNotFoundException e) {e.printStackTrace();}// java中任何一个对象都有一个方法:getClass()String s = "abc";Class x = s.getClass(); // x代表String.class字节码文件,x代表String类型。System.out.println(c1 == x); // true(==判断的是对象的内存地址。)Date time = new Date();Class y = time.getClass();System.out.println(c2 == y); // true (c2和y两个变量中保存的内存地址都是一样的,都指向方法区中的字节码文件。)// 第三种方式,java语言中任何一种类型,包括基本数据类型,它都有.class属性。Class z = String.class; // z代表String类型Class k = Date.class; // k代表Date类型Class f = int.class; // f代表int类型Class e = double.class; // e代表double类型System.out.println(x == z); // true}
}
/*
获取到Class,能干什么?通过Class的newInstance()方法来实例化对象。注意:newInstance()方法内部实际上调用了无参数构造方法,必须保证无参构造存在才可以。*/
public class ReflectTest02 {public static void main(String[] args) {// 这是不使用反射机制,创建对象User user = new User();System.out.println(user);// 下面这段代码是以反射机制的方式创建对象。try {// 通过反射机制,获取Class,通过Class来实例化对象Class c = Class.forName("com.bjpowernode.java.bean.User"); // c代表User类型。// newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。// 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!Object obj = c.newInstance();System.out.println(obj); // com.bjpowernode.java.bean.User@10f87f48} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}}
}
验证反射机制的灵活性。
java代码写一遍,再不改变java源代码的基础之上,可以做到不同对象的实例化。
非常之灵活。(符合OCP开闭原则:对扩展开放,对修改关闭。)
public class ReflectTest03 {public static void main(String[] args) throws Exception{// 这种方式代码就写死了。只能创建一个User类型的对象//User user = new User();// 以下代码是灵活的,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建出不同的实例对象。// 通过IO流读取classinfo.properties文件FileReader reader = new FileReader("chapter25/classinfo2.properties");// 创建属性类对象MapProperties pro = new Properties(); // key value都是String// 加载pro.load(reader);// 关闭流reader.close();// 通过key获取valueString className = pro.getProperty("className");//System.out.println(className);// 通过反射机制实例化对象Class c = Class.forName(className);Object obj = c.newInstance();System.out.println(obj);}
}
研究一下:Class.forName()发生了什么?
记住,重点:
如果你只是希望一个类的静态代码块执行,其它代码一律不执行,
你可以使用:
Class.forName(“完整类名”);
这个方法的执行会导致类加载,类加载时,静态代码块执行。
public class ReflectTest04 {public static void main(String[] args) {try {// Class.forName()这个方法的执行会导致:类加载。Class.forName("com.bjpowernode.java.reflect.MyClass");} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
/*
研究一下文件路径的问题。
怎么获取一个文件的绝对路径。以下讲解的这种方式是通用的。但前提是:文件需要在类路径下。才能用这种方式。*/
public class AboutPath {public static void main(String[] args) throws Exception{// 这种方式的路径缺点是:移植性差,在IDEA中默认的当前路径是project的根。// 这个代码假设离开了IDEA,换到了其它位置,可能当前路径就不是project的根了,这时这个路径就无效了。//FileReader reader = new FileReader("chapter25/classinfo2.properties");// 接下来说一种比较通用的一种路径。即使代码换位置了,这样编写仍然是通用的。// 注意:使用以下通用方式的前提是:这个文件必须在类路径下。// 什么类路径下?方式在src下的都是类路径下。【记住它】// src是类的根路径。/*解释:Thread.currentThread() 当前线程对象getContextClassLoader() 是线程对象的方法,可以获取到当前线程的类加载器对象。getResource() 【获 取资源】这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源。*/String path = Thread.currentThread().getContextClassLoader().getResource("classinfo2.properties").getPath(); // 这种方式获取文件绝对路径是通用的。// 采用以上的代码可以拿到一个文件的绝对路径。// /C:/Users/Administrator/IdeaProjects/javase/out/production/chapter25/classinfo2.propertiesSystem.out.println(path);// 获取db.properties文件的绝对路径(从类的根路径下作为起点开始)String path2 = Thread.currentThread().getContextClassLoader().getResource("com/bjpowernode/java/bean/db.properties").getPath();System.out.println(path2);}
}
/*
java.util包下提供了一个资源绑定器,便于获取属性配置文件中的内容。
使用以下这种方式的时候,属性配置文件xxx.properties必须放到类路径下。*/
public class ResourceBundleTest {public static void main(String[] args) {// 资源绑定器,只能绑定xxx.properties文件。并且这个文件必须在类路径下。文件扩展名也必须是properties// 并且在写路径的时候,路径后面的扩展名不能写。//ResourceBundle bundle = ResourceBundle.getBundle("classinfo2");ResourceBundle bundle = ResourceBundle.getBundle("com/bjpowernode/java/bean/db");String className = bundle.getString("className");System.out.println(className);}
}
/*
反射Student类当中所有的Field(了解一下)*/
public class ReflectTest05 {public static void main(String[] args) throws Exception{// 获取整个类Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");//com.bjpowernode.java.bean.StudentString className = studentClass.getName();System.out.println("完整类名:" + className);String simpleName = studentClass.getSimpleName();System.out.println("简类名:" + simpleName);// 获取类中所有的public修饰的Field//public int no;Field[] fields = studentClass.getFields();System.out.println(fields.length); // 测试数组中只有1个元素// 取出这个FieldField f = fields[0];// 取出这个Field它的名字String fieldName = f.getName();System.out.println(fieldName);// 获取所有的FieldField[] fs = studentClass.getDeclaredFields();System.out.println(fs.length); // 4System.out.println("==================================");// 遍历for(Field field : fs){// 获取属性的修饰符列表int i = field.getModifiers(); // 返回的修饰符是一个数字,每个数字是修饰符的代号!!!System.out.println(i); // 1,2,3,4// 可以将这个“代号”数字转换成“字符串”吗?String modifierString = Modifier.toString(i);System.out.println(modifierString);// 获取属性的类型Class fieldType = field.getType();//String fName = fieldType.getName(); String fName = fieldType.getSimpleName(); //stringSystem.out.println(fName);// 获取属性的名字System.out.println(field.getName()); // name age sex no }}
}
public class ReflectTest06 {public static void main(String[] args) throws Exception{// 创建这个是为了拼接字符串。StringBuilder s = new StringBuilder();//Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");Class studentClass = Class.forName("java.lang.Thread");s.append(Modifier.toString(studentClass.getModifiers()) + " class " + studentClass.getSimpleName() + " {\n");Field[] fields = studentClass.getDeclaredFields();for(Field field : fields){s.append("\t");s.append(Modifier.toString(field.getModifiers()));s.append(" ");s.append(field.getType().getSimpleName());s.append(" ");s.append(field.getName());s.append(";\n");}s.append("}");System.out.println(s);}
}
/*
必须掌握:怎么通过反射机制访问一个java对象的属性?给属性赋值set获取属性的值get*/
public class ReflectTest07 {public static void main(String[] args) throws Exception{// 我们不使用反射机制,怎么去访问一个对象的属性呢?Student s = new Student();// 给属性赋值s.no = 1111; //三要素:给s对象的no属性赋值1111//要素1:对象s//要素2:no属性//要素3:1111// 读属性值// 两个要素:获取s对象的no属性的值。System.out.println(s.no);// 使用反射机制,怎么去访问一个对象的属性。(set get)Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");Object obj = studentClass.newInstance(); // obj就是Student对象。(底层调用无参数构造方法)// 获取no属性(根据属性的名称来获取Field)Field noFiled = studentClass.getDeclaredField("no");// 给obj对象(Student对象)的no属性赋值/*虽然使用了反射机制,但是三要素还是缺一不可:要素1:obj对象要素2:no属性要素3:2222值注意:反射机制让代码复杂了,但是为了一个“灵活”,这也是值得的。*/noFiled.set(obj, 22222); // 给obj对象的no属性赋值2222// 读取属性的值// 两个要素:获取obj对象的no属性的值。System.out.println(noFiled.get(obj));// 可以访问私有的属性吗?Field nameField = studentClass.getDeclaredField("name");// 打破封装(反射机制的缺点:打破封装,可能会给不法分子留下机会!!!)// 这样设置完之后,在外部也是可以访问private的。nameField.setAccessible(true);// 给name属性赋值nameField.set(obj, "jackson");// 获取name属性的值System.out.println(nameField.get(obj));}
}
可变长度参数
int… args 这就是可变长度参数
语法是:类型… (注意:一定是3个点。)
1、可变长度参数要求的参数个数是:0~N个。
2、可变长度参数在参数列表中必须在最后一个位置上,而且可变长度参数只能有1个。
3、可变长度参数可以当做一个数组来看待
/*
作为了解内容(不需要掌握):反射Method*/
public class ReflectTest08 {public static void main(String[] args) throws Exception{// 获取类了Class userServiceClass = Class.forName("com.bjpowernode.java.service.UserService");// 获取所有的Method(包括私有的!)Method[] methods = userServiceClass.getDeclaredMethods();//System.out.println(methods.length); // 2// 遍历Methodfor(Method method : methods){// 获取修饰符列表System.out.println(Modifier.toString(method.getModifiers()));// 获取方法的返回值类型System.out.println(method.getReturnType().getSimpleName());// 获取方法名System.out.println(method.getName());// 方法的修饰符列表(一个方法的参数可能会有多个。)Class[] parameterTypes = method.getParameterTypes();for(Class parameterType : parameterTypes){System.out.println(parameterType.getSimpleName());}}}
}
/*
了解一下,不需要掌握(反编译一个类的方法。)*/
public class ReflectTest09 {public static void main(String[] args) throws Exception{StringBuilder s = new StringBuilder();//Class userServiceClass = Class.forName("com.bjpowernode.java.service.UserService");Class userServiceClass = Class.forName("java.lang.String");s.append(Modifier.toString(userServiceClass.getModifiers()) + " class "+userServiceClass.getSimpleName()+" {\n");Method[] methods = userServiceClass.getDeclaredMethods();for(Method method : methods){//public boolean login(String name,String password){}s.append("\t");s.append(Modifier.toString(method.getModifiers()));s.append(" ");s.append(method.getReturnType().getSimpleName());s.append(" ");s.append(method.getName());s.append("(");// 参数列表Class[] parameterTypes = method.getParameterTypes();for(Class parameterType : parameterTypes){s.append(parameterType.getSimpleName());s.append(",");}// 删除指定下标位置上的字符s.deleteCharAt(s.length() - 1);s.append("){}\n");}s.append("}");System.out.println(s);}
}
/*
重点:必须掌握,通过反射机制怎么调用一个对象的方法?五颗星*****反射机制,让代码很具有通用性,可变化的内容都是写到配置文件当中,将来修改配置文件之后,创建的对象不一样了,调用的方法也不同了,但是java代码不需要做任何改动。这就是反射机制的魅力。*/
public class ReflectTest10 {public static void main(String[] args) throws Exception{// 不使用反射机制,怎么调用方法// 创建对象UserService userService = new UserService();// 调用方法/*要素分析:要素1:对象userService要素2:login方法名要素3:实参列表要素4:返回值*/boolean loginSuccess = userService.login("admin","123");//System.out.println(loginSuccess);System.out.println(loginSuccess ? "登录成功" : "登录失败");// 使用反射机制来调用一个对象的方法该怎么做?Class userServiceClass = Class.forName("com.bjpowernode.java.service.UserService");// 创建对象Object obj = userServiceClass.newInstance();// 获取MethodMethod loginMethod = userServiceClass.getDeclaredMethod("login", String.class, String.class);//Method loginMethod = userServiceClass.getDeclaredMethod("login", int.class);// 调用方法// 调用方法有几个要素? 也需要4要素。// 反射机制中最最最最最重要的一个方法,必须记住。/*四要素:loginMethod方法obj对象"admin","123" 实参retValue 返回值*/Object retValue = loginMethod.invoke(obj, "admin","123123");System.out.println(retValue);}
}
/*
反编译一个类的Constructor构造方法。*/
public class ReflectTest11 {public static void main(String[] args) throws Exception{StringBuilder s = new StringBuilder();Class vipClass = Class.forName("java.lang.String");s.append(Modifier.toString(vipClass.getModifiers()));s.append(" class ");s.append(vipClass.getSimpleName());s.append("{\n");// 拼接构造方法Constructor[] constructors = vipClass.getDeclaredConstructors();for(Constructor constructor : constructors){//public Vip(int no, String name, String birth, boolean sex) {s.append("\t");s.append(Modifier.toString(constructor.getModifiers()));s.append(" ");s.append(vipClass.getSimpleName());s.append("(");// 拼接参数Class[] parameterTypes = constructor.getParameterTypes();for(Class parameterType : parameterTypes){s.append(parameterType.getSimpleName());s.append(",");}// 删除最后下标位置上的字符if(parameterTypes.length > 0){s.deleteCharAt(s.length() - 1);}s.append("){}\n");}s.append("}");System.out.println(s);}
}
/*
比上一个例子(ReflectTest11)重要一些!!!通过反射机制调用构造方法实例化java对象。(这个不是重点)*/
public class ReflectTest12 {public static void main(String[] args) throws Exception{// 不使用反射机制怎么创建对象Vip v1 = new Vip();Vip v2 = new Vip(110, "zhangsan", "2001-10-11", true);// 使用反射机制怎么创建对象呢?Class c = Class.forName("com.bjpowernode.java.bean.Vip");// 调用无参数构造方法Object obj = c.newInstance();System.out.println(obj);// 调用有参数的构造方法怎么办?// 第一步:先获取到这个有参数的构造方法Constructor con = c.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);// 第二步:调用构造方法new对象Object newObj = con.newInstance(110, "jackson", "1990-10-11", true);System.out.println(newObj);// 获取无参数构造方法Constructor con2 = c.getDeclaredConstructor();Object newObj2 = con2.newInstance();System.out.println(newObj2);}
}
/*
重点:给你一个类,怎么获取这个类的父类,已经实现了哪些接口?*/
public class ReflectTest13 {public static void main(String[] args) throws Exception{// String举例Class stringClass = Class.forName("java.lang.String");// 获取String的父类Class superClass = stringClass.getSuperclass();System.out.println(superClass.getName());// 获取String类实现的所有接口(一个类可以实现多个接口。)Class[] interfaces = stringClass.getInterfaces();for(Class in : interfaces){System.out.println(in.getName());}}
}
注解
3.2、注解Annotation是一种引用数据类型。编译之后也是生成xxx.class文件。
3.3、怎么自定义注解呢?语法格式?[修饰符列表] @interface 注解类型名{}
3.6、元注解什么是元注解?用来标注“注解类型”的“注解”,称为元注解。常见的元注解有哪些?TargetRetention关于Target注解:这是一个元注解,用来标注“注解类型”的“注解”这个Target注解用来标注“被标注的注解”可以出现在哪些位置上。@Target(ElementType.METHOD):表示“被标注的注解”只能出现在方法上。@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})表示该注解可以出现在:构造方法上字段上局部变量上方法上....类上...关于Retention注解:这是一个元注解,用来标注“注解类型”的“注解”这个Retention注解用来标注“被标注的注解”最终保存在哪里。@Retention(RetentionPolicy.SOURCE):表示该注解只被保留在java源文件中。@Retention(RetentionPolicy.CLASS):表示该注解被保存在class文件中。@Retention(RetentionPolicy.RUNTIME):表示该注解被保存在class文件中,并且可以被反射机制所读取。3.7、Retention的源代码//元注解 public @interface Retention {//属性RetentionPolicy value();}RetentionPolicy的源代码:public enum RetentionPolicy {SOURCE,CLASS,RUNTIME}//@Retention(value=RetentionPolicy.RUNTIME)@Retention(RetentionPolicy.RUNTIME)public @interface MyAnnotation{}
public @interface MyAnnotation {/*** 我们通常在注解当中可以定义属性,以下这个是MyAnnotation的name属性。* 看着像1个方法,但实际上我们称之为属性name。* @return*/String name();/*颜色属性*/String color();/*年龄属性*/int age() default 25; //属性指定默认值}//@MyAnnotation(属性名=属性值,属性名=属性值,属性名=属性值)//指定name属性的值就好了。@MyAnnotation(name = "zhangsan", color = "红色")public void doSome(){}
如果一个注解的属性的名字是value,并且只有一个属性的话,在使用的时候,该属性名可以省略。
// 数组是大括号@OtherAnnotation(age = 25, email = {"zhangsan@123.com", "zhangsan@sohu.com"}, seasonArray = Season.WINTER)public void doSome(){}
//只允许该注解可以标注类、方法
@Target({ElementType.TYPE, ElementType.METHOD})
// 希望这个注解可以被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {/*value属性。*/String value() default "北京大兴区";
}
java最全基础③进阶相关推荐
- Java改知能机_Java 面试突击之 Java 并发知识基础 进阶考点全解析
版权说明:本文内容根据 github 开源项目整理所得 项目地址:https://github.com/Snailclimb/JavaGuidegithub.com 一.基础 什么是线程和进程? 何 ...
- java最全基础知识_Java编程入门,计数排序(Counting Sort)怎么做?
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中. 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数. 计数排序(Counting sort)是一种稳定的排 ...
- java最全基础知识_Java编程入门,选择排序(Selection Sort)怎么做?
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好.唯一的好处可能就是不占用额外的内存空间了吧.理论上讲,选择排序可能也是平时排序一般人想到的 ...
- java多线程nullpointerexception_温故而知新!越是基础越容易被忽略,java最全基础知识,附赠资料...
一.hashMap与hashTable与ConcurrentHashMap: 1.HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类.不过它们都同时实现 ...
- 第二十八节:Java基础-进阶继承,抽象类,接口
前言 Java基础 - 进阶继承,抽象类,接口 进阶继承 class Stu {int age = 1; } class Stuo extends Stu {int agee = 2; } class ...
- java书籍_2020年java从入门到进阶书籍推荐,基础\自学\编程\数据结构\后端\虚拟机\网络\设计模式书籍...
前言 1. 基础书籍 2. 并发书籍 3. JVM虚拟机书籍 4. 网络相关 5. 操作系统 6. 数据结构与算法 7. 数据库 8. 设计模式 前言 从1996年到现在,java已经走过了24个年头 ...
- Java 面试全解析:核心知识点与典型面试题
课程背景 又逢"金九银十",年轻的毕业生们满怀希望与忐忑,去寻找.竞争一个工作机会.已经在职的开发同学,也想通过社会招聘或者内推的时机争取到更好的待遇.更大的平台. 然而,面试人群 ...
- java学习笔记-基础知识-2023.3.29
学习网站:https://www.sxt.cn/Java_jQuery_in_action/History_Direction.html 刷题网站 牛客网专题-spring java sql 第一章 ...
- Java学完基础语法之后,应该学什么?
学习Java对于刚刚开始接触的人来说,通常都是比较懵的,计算机语言都很复杂,学习需要系统化,流程化的一步步来.许多人刚刚开始学习Java不知道先学什么?这里可以明确告诉大家,先学基础语法是肯定没错的, ...
最新文章
- STM32F4 HAL库开发 -- 再识
- 把数据导出Excel
- Linux学习之系统编程篇:守护进程(精灵进程、后台进程)
- vue中使用Ueditor编辑器 -- 1
- python网络编程基础知识_python网络编程基础
- Codeforces Round #829 (Div. 2) C1. Make Nonzero Sum (easy version) 解题报告
- 信息规范和信息安全素养计算机,技师学院计算机专业学生信息安全意识培养的教学研究...
- 竞价单页设计需要了解的知识
- c51编译器+linux,C51 开源编译器SDCC学习笔记-安装
- 配置香橙派zeropuls2
- H5活动之家平台,开启国庆双十一福利,活动全免费
- 前端大佬谈国产开源:VUE 的成功在于社区运营
- 小米电视是鸿蒙系统吗,搭载鸿蒙系统!华为正式发布荣耀智慧屏 小米电视迅速升级取消开机广告...
- 机器学习算法-EM算法
- java基础案例 饲养员喂养动物
- 辐射76 服务器位置,《辐射76》确认没有NPC 但将开放私人服务器与MOD
- 怀揣坚定与执着,踏实稳步向前
- html与网页设计 实践课,HTML5网页设计课程混合式教学设计与实践
- Flask允许跨域请求
- selenium网页自动登录、打卡(二)
热门文章
- Bishop.Pattern Recognition and Machine learning(一)
- 利用500W条微博语料对评论进行情感分析
- rcar-du display timing generation
- JavaFX 更换程序图标 报错Invalid URL: unknown protocol: c
- 直播怎样吸引人,留住人,直播和粉丝互动的方法
- 什么是无货源店群模式,拼多多开店后怎么运营,适合什么人做
- 可怕!简直了! 无人机竟然“操控”了机场!
- 中国变频器行业需求预测与投资战略规划分析报告2022-2028年版
- IEEE1394接口
- 计算机体系结构(复习资料)