java HelloWorld 的执行过程以及原理
- 第一步:启动 Java 虚拟机(JVM)
- 第二步:虚拟机启动后,虚拟机会启动 类加载器 classloader
- 类加载器的作用:加载类。本质上就是类加载器负责去硬盘上找"类"对应的"字节码"文件
- 例如 java helloworld 那么类加载文件就会去找 helloworld.class 文件。
- 第三步:类加载器如果在硬盘上找不到对应的字节码文件,会报错,无法加载主类
- 类加载器如果找到对应的字节码文件,类加载器会将字节码文件装载到 JVM 当中,JVM 启动”解释器“将字节码解释为”101010000...“这种二进制码,操作系统执行二进制文件和硬件交互。
Java 数据类型
基本数据类型 8 种
- 整数型 byte(字节型)、short(短整型)、int(整型)、long (长整型)
- 浮点型 double(双精度)、float(单精度)
- 布尔型 boolean
- 字符型 char
引用数据类型:Java 中除了基本数据类型之外都是引用数据类型。
- String 字符串不属于基本数据类型
计算机存储单位
计算机只能识别二进制。(10011100...)
1 字节=8bit(8 比特)--> 1byte=8bit
1bit 就是一个 1 或 0
1KB=1024byte、1MB=1024KB、1GB=1024MB、1TB=1024GB
byte b=2:在计算机中表示为 00000010
short s=2:在计算机中表示为 00000000 00000010
int i=2:在计算机中表示为 00000000 00000000 00000010
byte-->1 字节(-128
127) short-->2 字节(-3276832767) int-->4 字节(-2147483648~2147483647) long-->8 字节float-->4 字节 double-->8 字节
boolean-->1 字节
char-->2 字节(0~65535)可以表示 65536 个不同的数字,和 short 相同
int a=10; System.out.println(a);//默认十进制 10 int b=010; System.out.println(b);//八进制 8 int c=0x10; System.out.println(c);//十六进制 16 int d=0b10; System.out.println(d);//二进制 2
ASCII('a'是 97 'A'是 65 '0'是 48)
计算机原码反码补码
计算机存储的是二进制补码形式
int i = 1;//正整数的原码反码补码相同 //对应的二进制原码:00000000 00000000 00000000 00000001 //对应的二进制反码:00000000 00000000 00000000 00000001 //对应的二进制补码:00000000 00000000 00000000 00000001 byte b = 1; //对应的二进制原码(第一位是符号位:负数的符号位为1):10000001 //对应的二进制反码(符号位不变,其他位取反):10000001 ==》 11111110 //对应的二进制补码(反码+1):11111110 ==》 11111111
数据类型混合运算的时候取最大类型的字节在继续做运算
逻辑判断符
//&与&&的区别
int x = 10;
int y = 11;
System.out.println(x > y & x > y++);//false
System.out.println(y);//12
int n = 10;
int m = 11;
System.out.println(n > m && n > m++);//false
System.out.println(m);//11
Java 方法
普通方法
public class test02 { public static void main(String[] args) { Print_Str("打印一个方法体里面的内容");//打印一个方法体里面的内容 } //[ 修饰符列表 ] 返回值类型 方法名称 方法参数 //[]修饰符参数为可选参数 //返回值类型包括基本数据类型和引用数据类型 public static void Print_Str(String st) { //void表示空类型,没有返回值类型 System.out.println(st); //方法体 if(big(4,5)){ System.out.print("是的大于"); }else { System.out.print("不是小于");//不是小于 } } public static Boolean big(int x,int y){ if(x>y){ return true; }else { return false; } } }
方法的执行顺序
方法体中的代码遵循自上而下的顺序依次执行
public class test03 { public static void main(String[] args) { print("main执行"); m1(); print("main结束"); } public static void m1() { print("m1执行"); m2(); print("m1结束"); } public static void m2() { print("m2执行"); myClass.m3(); print("m2结束"); } public static void print(String str) { System.out.println(str); } } class myClass { public static void m3() { test03.print("m2执行"); System.out.println("这是m3方法!"); test03.print("m2结束"); } } //打印输入结果 main执行 m1执行 m2执行 m2执行 这是m3方法! m2结束 m2结束 m1结束 main结束break 和 return 的区别
- break 控制的是循环;终止循环
- return 控制的是方法;终止当前方法
Java 方法重载
条件 1:在同一个类当中
条件 2:方法名相同
条件 3:
参数列表不同
- 参数个数不同
- 参数类型不同
- 参数顺序不同
注意:
- 和返回类型无关
- 和修饰符无关
public class test06 { public static void main(String[] args) { System.out.println(m1(10, 1));//11 double a = m1(10.8, 1.8); System.out.println(String.format("%.2f", a));//12.60 } public static int m1(int x, int y) { return x + y; } public static double m1(double x, double y) { return x + y; } } //重载 println方法 System.out.println(1.8d); System.out.println(1977777777777777777L); System.out.println(456); System.out.println(true);
Java 递归
public class RecursionTest02 { public static void main(String[] args) { int result=getSum(3); System.out.println(result); } public static int getSum(int n) { if (n == 1) { return 1; } return n + getSum(--n); } }

面向对象和面向过程的区别
- 从语言方面
- 对于 c 语言来说,是完全面向过程的。
- 对于 c++语言来说,是一半面向过程,一半是面向对象(c++半面向对象)
- 对于 Java 和 c#语言来说,是完全面向对象的。
- 面向过程开发
- 面向过程的开发方式主要特点是:
- 注重步骤,注重的是实现这个功能的步骤
- 第一步做什么
- 第二步做什么
- ...
- 另外面向过程也注重实现功能的因果关系
- 因为 A 所以 B
- 因为 B 所以 C
- ...
- 面向过程中没有对象的概念。只是实现这个功能的步骤以及因果关系。
- 面向过程的优缺点
- 缺点:耦合度高拓展力差
- 面向过程最主要的是每一步与每一步的因果关系,其中 A 步骤因果关系到 B 步骤,A 和 B 联合起来形成一个子模块,子模块和子模块之间又因为因果关系结合在一起,假设其中任何一个因果关系出现问题(错误),此时整个系统的运转都会出现问题。(代码和代码之间的耦合度太高,拓展性太差)
- 耦合度高导致拓展力差(集成显卡:计算机显卡不是独立显卡,是集成在主板上)
- 耦合度低导致拓展力强(灯泡和灯口关系,螺栓和螺母关系)
- 集成显卡和独立显卡=》面向过程和面向对象
- 面向过程最主要的是每一步与每一步的因果关系,其中 A 步骤因果关系到 B 步骤,A 和 B 联合起来形成一个子模块,子模块和子模块之间又因为因果关系结合在一起,假设其中任何一个因果关系出现问题(错误),此时整个系统的运转都会出现问题。(代码和代码之间的耦合度太高,拓展性太差)
- 优点:快速开发
- 对于小型项目(功能),采用面向过程的方式进行开发,效率较高。不需要前期进行对象的提取,模型的建立,采用面向过程方式可以直接开始干活。一上来直接写代码,编写因果关系。从而实现功能。
- 缺点:耦合度高拓展力差
- 面向过程的开发方式主要特点是:
- 面向对象开发
- 面向对象的开发方式主要特点是:
- 更符合人类的思维方式。(面向对象成为主流的原因)
- 人类就是以
对象的方式去认识世界的。 - 面向对象就是将现实世界分割成不同的单元,然后每一个单元都实现成对象,然后给一个环境驱动一下。让各个对象之间协作起来形成一个系统。

- 特点:耦合度低,拓展力强。
- 面向过程主要关注的是:实现步骤以及整个过程。
- 面向对象主要关注的是:对象 A,对象 B,对象 C,然后对象 ABC 组合,或者 CBA 组合...
- 面向对象的开发方式主要特点是:
- 当我们采用面向对象的方式贯穿整个系统的话,涉及到的三个术语:
- OOA:面向对象分析
- OOD:面向对象设计
- OOP:面向对象编程
- 实现一个软件的过程:
- 分析(A)-->设计(D)-->编程(p)
- 类和对象
- 类:是一个抽象的概念,不存在的,人类大脑思考总结一个模板(这个模板描述了共同特征),对象特征的总结
- 具有共同特征,抽象出来的东西,就是一个类
- 对象:实际存在的个体(人是一个类,姚明是一个对象...)
- 实例:对象还有一个名字叫做实例 Stu s=new Stu(),s 为实例
- 实例化:通过类这个模板创建对象的过程,叫做:实例化
- 抽象:多个对象具有共同特征,进行思考总结抽取共同特征的过程
- 类 -- 【实例化】--> 对象(实例)
- 对象 -- 【抽象】--> 类



- 类:是一个抽象的概念,不存在的,人类大脑思考总结一个模板(这个模板描述了共同特征),对象特征的总结
Java 工程师与现实世界的关系
创建对象
public class test07 { public static void main(String[] args) { Student s1=new Student(); s1.name="张三"; System.out.println(s1.name); System.out.println("-------------------"); Student s2=new Student(); s2.name="李四"; System.out.println(s2.name); } } class Student { //学生名字 String name; //学号 int number; //性别 boolean sex; //地址 String address; }

引用和对象区分
- 引用是存储对象内存地址的一个变量。
- 对象是堆内存里面 new 出来的。
package com.cobj.test01; public class Addres { //城市 String city; //街道 String street; //邮编 int zipcode; } package com.cobj.test01; public class User { int id;//成员变量,实例变量(对象变量) String name; Addres addres; } package com.cobj.test01; //第一步:类加载 public class Test { //第二步:test调用main方法(方法调用要压栈) //String name; 成员变量 public static void main(String[] args){ User u1=new User(); u1.addres=new Addres(); u1.addres.street="瑶湖区"; u1.addres.city="南昌"; u1.addres.zipcode=331200; u1.name="张三"; System.out.println(u1.name+','+u1.addres.street+','+u1.addres.zipcode); } }
对象回收
垃圾回收器:
GC- 在 Java 语言中,垃圾回收器主要针对的是**
堆内存** - 当一个 Java 对象没有任何引用指向该对象的时候
- GC 会考虑将该垃圾数据释放回收掉
- 在 Java 语言中,垃圾回收器主要针对的是**

空指针异常(NullPointerException)
- **
空引用**访问实例【对象相关】相关的数据时,都会出现空指针异常。
- **
方法调用参数传递
Java 中关于方法调用时参数传递实际只有一个规则:
- 在参数传递的时候,将变量中保存的那个
值复制一份过去
- 在参数传递的时候,将变量中保存的那个
public class Test01 { public static void main(String[]args){ int i=10; add(i); System.out.println("main=>"+i);//10 } public static void add(int i){ i++; System.out.println("add=>"+i);//11 } } //基于对象传值 public class Test { public static void main(String[] args) { Persion p = new Persion(); p.age = 10; add(p); System.out.println("main=>" + p.age);//11 } public static void add(Persion p) { p.age++; System.out.println("add=>" + p.age);//11 } } class Persion { int age; }
构造方法
什么是构造方法,有什么用?
- 构造方法是一个比较特殊的方法,通过构造方法可以完成对象的创建,以及实例变量的初始化。换句话说:构造方法是用来创建对象,并且同时给对象的属性赋值。(注意:实例变量没有手动赋值的时候,系统会赋默认值
- 重点:当一个类没有提供任何构造方法,系统会默认提供一个无参数的构造方法。(而这个构造方法被称为缺省构造器)
调用构造方法怎么调用
使用 new 运算符来调用构造方法
public class Test { public static void main(String [] args){ Student st=new Student(19,"张三","REP"); System.out.println("年龄"+st.age);//年龄19 } } public class Student { String name; int Number; int age; String hb; public Student(int age,String name,String hb) {//构造方法 this.name=name; this.age=age; this.hb=hb; System.out.println("我是"+this.name);//张三 } }
构造方法的语法结构
- 构造方法名和类名必须一致
普通方法的语法结构
实例变量没有手动赋值的时候,实际上系统会默认赋值
- 赋值操作在什么时间进行
- 实例变量是在构造方法执行的过程中完成初始化赋值
- 错误 ❌ 不是在类加载的时候给实例变量赋值
- 赋值操作在什么时间进行
封装
面向对象的首要特征:封装 。什么是封装?有什么用?
现实生活中有很多现实的例子都是封装的,
例如:手机,电视机,笔记本电脑,照相机,这些都是外部有一个坚硬的壳儿。封装起来,保护内部的部件。保证内部的部件是安全的。另外封装了之后,对于我们使用者来说,我们是看不见内部的复杂结构的,我们也不需要关心内部有多么复杂,我们只需要操作外部壳儿上的几个按钮就可以完成操作。
封装的作用有两个:
- 第一个作用:保证内部结构的安全。
- 第二个作用:屏蔽复杂,暴露简单。
代码级别上的作用
- 一个类体当中的数据,假设封装之后,对于代码的调用人员来说,不需要关心代码的复杂实现,只需要通过一个简单的入口就可以访问了。另外,类体中安全级别较高的数据封装起来,外部人员不能随意访问,来保证数据的安全性。
怎么进行封装,代码怎么实现? 第一步:属性私有化(使用 private 关键字进行修饰。) 第二步:对外提供简单的操作入口。
public class Student { private String name; private int Number; private int age; private String hb; public Student(int age, String name, String hb) { this.name = name; this.age = age; this.hb = hb; System.out.println("我是" + this.name);//张三 } public void setAge(int age) { if (age <= 0 || age >= 150) { System.out.println("年龄设置不合法"); return; } this.age = age; } public int getAge() { return age; } } public class Test { public static void main(String [] args){ Student st=new Student(19,"张三","REP"); System.out.println("年龄"+st.getAge());//年龄19 st.setAge(-18); System.out.println("年龄"+st.getAge());//年龄19 } }
静态
class VarTest{ /* * 成员变量: * 实例变量 * 静态变量 * */ //以下实例的,都是对象相关的,访问时采用“引用.”的方式访问。需要先new对象 //实例相关的,必须先有对象,才能访问,可能会出现空指针异常的情况。 int i;//成员变量中的实例变量 public void m2(){}//实例方法 //以下静态的,都是类相关的,访问时采用“类名.”的方式访问。不需要new对象 //不需要对象的参与即可访问。没有空指针异常的发生。 //静态变量在类加载的时候初始化,不需要new对象,静态变量的空间就开出来了。 //静态变量存储在方法区 static int n;//成员变量中的静态变量 public static void m1(){}//静态方法 }静态变量
- 如果这个类型的所有对象的某个属性都是一样的,不建议定义为实例变量,浪费内存空间。建议定义为类级别特征。定义为静态变量

代码执行顺序
public class Test { //代码执行顺序 static { System.out.println("A"); } public static void main(String[] args) { System.out.println("C"); new Test(); System.out.println("F"); //ABCDEF } public Test() { System.out.println("E"); } { System.out.println("D"); } static { System.out.println("B"); } }
This
this 是一个关键字
一个对象一个 this
- this 是一个变量,是一个引用。this 保存当前对象的内存地址,指向自身。所以严格意义上来说,this 代表的就是
当前对象,this 存储在堆内存当中对象的全部。 - this 不能使用在静态方法中。
- this 是一个变量,是一个引用。this 保存当前对象的内存地址,指向自身。所以严格意义上来说,this 代表的就是
package com.This; public class Shop { String name; public Shop(String name){ this.name=name; } public void Look(){ System.out.println(this); System.out.println(this.name+"正在购物!"); } } package com.This; public class Test { public static void main(String[] args) { Shop s=new Shop("张三"); System.out.println(s); s.Look(); //com.This.Shop@1b6d3586 //com.This.Shop@1b6d3586 //张三正在购物! Shop s1=new Shop("李四"); System.out.println(s1); s1.Look(); //com.This.Shop@4554617c //com.This.Shop@4554617c //李四正在购物! } }this()
public Date(){ //this.year=1970; //this.month=1; //this.day=1; this(1970,1,1);//更优 } public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }

变量内存图
总结(实例、静态、构造)
public class Sutdent {//类 static { System.out.println("Student类开始加载"); } //类加载机制中:在程序执行之前,凡是需要加载的类全部加载到JVM当中。 //先完成加载才会执行main方法 public static void main(String[] args) { //局部变量 int i = 100; Stu s = new Stu("张😀", 2021001); s.study(); } } class Stu { static { System.out.println("Stu类开始加载"); } /* 类体{ 实例变量 实例方法 静态变量 静态方法 构造方法 静态代码语句块 实例代码语句块 方法(){ 局部变量 int i =100 } } */ private String name;//实例变量 private int idCard;//实例变量 static String job = "学生";//静态变量 //构造方法 public Stu() { this("未知学生名字", 00001);//this直接调用构造方法赋默认值 } public Stu(String name, int idCard) { this.name = name; this.idCard = idCard; } //实例方法 //静态方法的调用使用 “引用”. public void study() { //私有的是可以在本类访问,其他类中需要使用get取值set赋值 //System.out.println(this.name+"正在学习!"); //System.out.println(name+"正在学习!"); //System.out.println(this.getName()+"正在学习!"); System.out.println(getName() + "正在学习!"); this.eat(); } public void eat() { System.out.println(this.getName() + "正在吃饭!"); Stu.m1(); } //静态方法 //静态方法的调用使用 “类名”. public static void m1() { System.out.println("执行了m1静态方法!"); Stu.m2(); } public static void m2() { System.out.println("m2方法访问静态变量Job:" + job); } //getter和setter方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public int getIdCard() { return idCard; } public void setIdCard(int idCard) { this.idCard = idCard; } }
继承
继承的相关特性
B 类继承 A 类,则称 A 类为超类(superclass)、父类、基类,B 类则称为子类(subclass)、派生类、扩展类。 class A class B extends A 我们平时聊天说的比较多的是:父类和子类。 superclass 父类 subclass 子类
java 中的继承只支持单继承,不支持多继承,C++中支持多继承,这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码:
class B extends A,C{ } 这是错误的。虽然 java 中不支持多继承,但有的时候会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说,C 直接继承 B,其实 C 还间接继承 A。
java 中规定,子类继承父类,除构造方法不能继承之外,剩下都可以继承。但是私有的属性无法在子类中直接访问。父类中 private 修饰的不能在子类中直接访问。可以通过间接的手段来访问(getter 和 setter 访问、赋值)。
java 中的类没有显示的继承任何类,则默认继承 Object 类,Object 类是 java 语言提供的根类(老祖宗类),也就是说,一个对象与生俱来就有 Object 类型中所有的特征。
继承也存在一些缺点,例如:CreditAccount 类继承 Account 类会导致它们之间的耦合度非常高,Account 类发生改变之后会马上影响到 CreditAccount 类
public class InheriTest { public static void main(String [] args){ Dog d=new Dog("小狗"); d.eat(); Cat c=new Cat("小猫"); c.eat(); } } class Animal{ String name; public void eat(){ System.out.println(this.name+"正在吃饭!"); } } class Dog extends Animal{ public Dog(String name){ this.name=name; } } class Cat extends Animal{ public Cat(String name){ this.name=name; } }
System.out.println
public class Test { static Student stu = new Student(); public static void main(String[] args) { stu.name = "张三"; Test.stu.study(); System.out.println("Hello word!"); } } class Student { String name; public void study() { System.out.println(this.name + "正在学习"); } }
方法覆盖
条件一:两个类必须要有继承关系。
条件二:重写之后的方法和之前的方法具有:
- 相同的返回值类型
- 相同的方法名
- 相同的形式参数列表
条件三:访问权限不能更低,可以更高(protected==>public)
条件四:重写之后的方法不能比之前的方法抛出更多的异常,可以更少。
注意:
- 方法覆盖只是针对于方法,和属性无关
- 私有方法无法覆盖
- 构造方法不能被继承,所以构造方法不能被覆盖
- 方法覆盖针对的是
实例方法,静态方法覆盖没有意义
public class Test { public static void main(String[]args){ Dog d=new Dog("小花狗"); d.move(); Cat c=new Cat("小懒猫"); c.move(); } } class Animal{ String name; public void move(){ System.out.println(this.name+"正在移动!"); } } class Dog extends Animal{ public Dog(String name){ this.name=name; } @Override public void move() { System.out.println(this.name+"正在奔跑!"); } } class Cat extends Animal{ public Cat(String name){ this.name=name; } //@Override //public void move() { // System.out.println(this.name+"正在路上爬!"); //} }
多态
父类型引用指向子类型对象
包括编译阶段和运行阶段
- 编译阶段:绑定父类的方法
- 编译阶段编译器只知道 d 的类型是 Animal,所以编译器在检查语法的时候,会去 Animal.class 字节码中找 move()方法。进行静态绑定(前提 Animal 中有 move 方法)
- 运行阶段:动态绑定子类型对象的方法
- 运行阶段的时候,实际上在堆内存中创建的 Java 对象是 Dog 对象,所以 move 的时候,真正参与 move 的对象是 Dog 对象。动态执行 Dog 对象的 move()方法(动态绑定)
- 多种形态 ,多种状态,编译和运行有两个不同的状态
- 编译阶段:绑定父类的方法
public class Animal { String name; public void move() { System.out.println(this.name + "在移动!"); } } public class Cat extends Animal { public Cat(String name) { this.name = name; } @Override public void move() { System.out.println(this.name + "在床上爬!"); } } public class Dog extends Animal { public Dog(String name) { this.name = name; } @Override public void move() { System.out.println(this.name + "在路上跑"); } public void likeFood(){ System.out.println(this.name+"爱吃骨头"); } } public class Test { public static void main(String[] args) { //向上转型 Animal d=new Dog("小花狗"); d.move();//小花狗在路上跑 //向下转型 Animal d1=new Dog("小狗狗"); if(d1 instanceof Dog){//判断对象类型 Dog dx=(Dog) d1; dx.likeFood(); } } }
多态应用
public class Test { public static void main(String [] args){ Person p=new Person(); Dog d=new Dog(); p.eats(d); Cat c=new Cat(); p.eats(c); } } class Person{ public void eats(Animal a){ a.eat(); } //面向父类型编程,面向更加抽象进行编程,不建议面向具体编程。面向具体编程会让软件的拓展力很差 //public void eats(Dog a){ // a.eat(); //} //public void eats(Cat a){ // a.eat(); //} } class Animal{ public void eat(){ } } class Dog extends Animal{ @Override public void eat() { System.out.println("狗正在吃骨头"); } } class Cat extends Animal{ @Override public void eat() { System.out.println("猫正在吃鱼"); } }静态方法不谈覆盖 static
私有方法不能覆盖 private
多态在开发中的作用是: 降低程序的耦合度,提高程序的扩展力。
public class Master{ public void feed(Dog d){} public void feed(Cat c){} }以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力很差。
public class Master{ public void feed(Pet pet){ pet.eat(); } }以上的代表中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类。 这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性。
Super
super 能出现在实例方法和构造方法中。
- super 的语法是:“super.”、“super()”
- super 不能使用在静态方法中。
- super. 大部分情况下是可以省略的。super.什么时候不能省略呢?
- 父类和子类中有同名属性,或者说有同样的方法,
- 想在子类中访问父类的,super. 不能省略。
- super() 只能出现在构造方法第一行,通过当前的构造方法去调用“父类”中 的构造方法,目的是:创建子类对象的时候,先初始化父类型特征。
- super 的使用: super.属性名 【访问父类的属性】 super.方法名(实参) 【访问父类的方法】 super(实参) 【调用父类的构造方法】
// 示例代码 public class test02 { public static void main(String []args){ Stu s=new Stu("张三",12,202101,"学生卡"); System.out.println(s.getName()+",拥有:"+s.getType()+",卡号:"+s.getCid()); Tea t=new Tea("梁老师",12,202101,"老师卡"); System.out.println(t.getName()+",拥有:"+t.getType()+",卡号:"+t.getCid()); } } class Card{ private int cid; private String type; public Card(){ System.out.println("Card"); } public Card(String type,int cid){ this.type=type; this.cid=cid; } public int getCid() { return cid; } public void setCid(int cid) { this.cid = cid; } public String getType() { return type; } public void setType(String type) { this.type = type; } } class Stu extends Card{ private String name; private int age; public Stu(){ } public Stu(String name,int age,int cid,String type){ super(type,cid); this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } class Tea extends Card{ private String name; private int age; public Tea(){ } public Tea(String name,int age,int cid,String type){ //super(type,cid); this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
final
final 是 Java 语言中的一个关键字
final 表示最终的,不可变的,只能赋一次值
final 可以修饰变量、方法、类等
final 修饰的方法无法覆盖和重写
final 修饰的类无法继承

final 修饰的实例变量,系统不负责赋默认值,必须手动赋值。在构造方法中赋值或者在变量后面赋值。
public class Tc { public static void main(String[] args) { BAB b = new BAB("张三"); BAB V = new BAB("三"); } } class BAB { final private String name; final private int age = 18; public BAB(String name) { this.name = name;//必须在构造方法中 } public void setName(String name) { // this.name=name;在此赋值报错 } public String getName() { return name; } }
抽象类(abstract)
抽象类定义
abstract class AbsTest{ }
抽象类:无法实例化、无法创建对象,抽象类是用来被子类继承的
final 和 abstract 不能联合使用,这两个关键字是对立的。final 修饰的类无法继承
抽象类的子类可以是抽象类,也可以是非抽象类
抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法供子类使用的
抽象类中不一定有抽象方法,抽象方法必须在抽象类中
抽象方法定义
public abstract void dosome();
一个非抽象类继承抽象类,必须将抽象类中的抽象方法进行覆盖、重写、实现
abstract class AbsTest { public abstract void dosome(); } class Test extends AbsTest { @Override public void dosome() { } }
接口(interface )
接口时一种“引用数据类型”
接口时完全抽象的
接口的定义
- [修饰符列表] interface 接口名
接口支持多继承
接口中只有常量+抽象方法
接口中所有的元素都是 public 修饰的
接口中抽象方法的 public abstract 可以省略
接口中常量的 public static final 可以省略
接口中方法不能有方法体
一个非抽象的类,实现接口的时候,必须将接口中所有方法加以实现
一个类可以实现多个接口
extends 和 implements 可以共存,extends 在前,implements 在后
使用接口,写代码的时候,可以使用多态(父类型引用指向子类型对象)
public class Test01 { public static void main(String[] args) { A e = new E(); double pi = e.PI; System.out.println(pi);//3.1415926 } } interface A { double PI = 3.1415926; void doSome(); } interface B { void doEat(); } interface C { } interface D extends A, B, C { } interface F { void doFly(); } class E extends G implements D,F { @Override public void doSome() { } @Override public void doEat() { } @Override public void doFly() { } } class G{ }
接口在开发中的作业
注意:接口在开发中的作用,类似于多态在开发中的作用。
多态:面向抽象编程,不要面向具体编程。降低程序的耦合度。提高程序的扩展力。
三个字“解耦合”
- 面向接口编程,可以降低程序的耦合度,提高程序的扩展力。符合 OCP 开发原则。
- 接口的使用离不开多态机制。(接口+多态才可以达到降低耦合度。)
- 接口可以解耦合,解开的是谁和谁的耦合!!! 任何一个接口都有调用者和实现者。 接口可以将调用者和实现者解耦合。 调用者面向接口调用。 实现者面向接口编写实现。
public class Test02 { public static void main(String[] args) { FoodMenu c = new WesternChef(); //实例化厨师 User u = new User(); //实例化用户 u.setFoodMenu(c); //给用户菜单 u.order(); //用户已经点好餐 } } //菜单接口 interface FoodMenu { void YuxiangPork(); void ScrambledEggs(); } //厨师实现炒菜接口 class ChinaChef implements FoodMenu { @Override public void YuxiangPork() { System.out.println("中国厨师做的鱼香肉丝"); } @Override public void ScrambledEggs() { System.out.println("中国厨师做的西红柿草鸡蛋"); } } class WesternChef implements FoodMenu { @Override public void YuxiangPork() { System.out.println("西餐厨师做的鱼香肉丝"); } @Override public void ScrambledEggs() { System.out.println("西餐厨师做的西红柿炒鸡蛋"); } } //用户点菜 class User { private FoodMenu foodMenu; public User() { } public User(FoodMenu foodMenu) { this.foodMenu = foodMenu; } public FoodMenu getFoodMenu() { return foodMenu; } public void setFoodMenu(FoodMenu foodMenu) { this.foodMenu = foodMenu; } //用户点餐 public void order() { //用户点了两个菜 this.foodMenu.YuxiangPork(); this.foodMenu.ScrambledEggs(); } }
类型和类型之间的关系
- 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 关系的表示“关联关系”
- 关联关系通常以“属性”的形式存在。
java A{ B b; }
- like a:
- Cooker like a FoodMenu(厨师像一个菜单一样)
- 凡是能够满足 like a 关系的表示“实现关系”
- 实现关系通常是:类实现接口。
- A implements B
抽象类和接口有什么区别
在这里我们只说一下抽象类和接口在语法上的区别。 至于以后抽象类和接口应该怎么进行选择,通过后面的项目去体会/学习。 抽象类是半抽象的。 接口是完全抽象的。 抽象类中有构造方法。 接口中没有构造方法。 接口和接口之间支持多继承。 类和类之间只能单继承。 一个类可以同时实现多个接口。 一个抽象类只能继承一个类(单继承)。 接口中只允许出现常量和抽象方法。 这里先透露一个信息: 以后接口使用的比抽象类多。一般抽象类使用的还是少。 接口一般都是对“行为”的抽象。
package 和 import
package
- 第一:package 出现在 java 源文件第一行。
- 第二:带有包名怎么编译?javac -d . xxx.java
- 第三:怎么运行?java 完整类名
- 补充:以后说类名的时候,如果带着包名描述,表示完整类名。如果没有带包,描述的话,表示简类名。
- java.util.Scanner 完整类名。
- Scanner 简类名
import
import 什么时候不需要?
- java.lang 不需要。
- 同包下不需要。
- 其它一律都需要。
怎么用?
- import 完整类名;
- import 包名.*;
- import java.util.Scanner; // 完整类名。
// 同学的疑问:这样是不是效率比较低。 // 这个效率不低,因为编译器在编译的时候,会自动把*变成具体的类名。 import java.util.*; // 想省懒劲你不能太省了。 import java.*; 这是不允许的,因为在java语言中规定,这里的*只代表某些类的名字。
访问控制权限
private 表示私有的,只能在本类中访问
public 表示公开的,在任何位置都可以访问
“默认”表示只能在本类,以及同包下访问。
protected 表示只能在本类、同包、子类中访问。
访问控制修饰符 本类 同包 子类 任意位置 public 可以 可以 可以 可以 protected 可以 可以 可以 访问不到 默认 可以 可以 访问不到 访问不到 private 可以 访问不到 访问不到 访问不到 范围从大到小排序:public > protected > 默认 > private
访问控制符用在方法中
- 属性(4 个都能用)
- 方法(4 个都能用)
- 类(public 和默认能用,其它不行。)
- 接口(public 和默认能用,其它不行。)
数组
数组是引用数据类型,所以数组对象是存储堆内存当中。
数组一旦创建长度不可变
数组中元素要求类型统一
数组中的元素存储地址是连续的,内存地址连续,数组存储元素的特点
数组的优缺点
- 第一:空间存储上,内存地址是连续的。
- 第二:每个元素占用的空间大小相同。
- 第三:知道首元素的内存地址。
- 第四:通过下标可以计算出偏移量。
- 通过一个数学表达式,就可以快速计算出某个下标位置上元素的内存地址,直接通过内存地址定位,效率非常高
- 优点:检索效率高
- 缺点:随机增删效率较低,数组无法存储大数据量
- 注意:数组最后一个元素的增删效率不受影响
一维数组的静态初始化和动态初始化
静态初始化:
int[] arr = {1,2,3,4}; Object[] objs = {new Object(), new Object(), new Object()};动态初始化:
int[] arr = new int[4]; // 4个长度,每个元素默认值0 Object[] objs = new Object[4]; // 4个长度,每个元素默认值null
冒泡排序
冒泡排序算法 1、每一次循环结束之后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡。) 2、核心: 拿着左边的数字和右边的数字比对,当左边 > 右边的时候,交换位置。 原始数据: 3, 2, 7, 6, 8 第1次循环:(最大的跑到最右边。) 2, 3, 7, 6, 8 (3和2比较,2 < 3,所以2和3交换位置) 2, 3, 7, 6, 8 (虽然不需要交换位置:但是3和7还是需要比较一次。) 2, 3, 6, 7, 8 (7和6交换位置) 2, 3, 6, 7, 8 (虽然不需要交换位置:但是3和7还是需要比较一次。) 经过第1次循环,此时剩下参与比较的数据:2, 3, 6, 7 第2次循环: 2, 3, 6, 7 (2和3比较,不需要交换位置) 2, 3, 6, 7 (3和6比较,不需要交换位置) 2, 3, 6, 7 (6和7比较,不需要交换位置) 经过第2次循环,此时剩下参与比较的数据:2, 3, 6 第3次循环: 2, 3, 6 (2和3比较,不需要交换位置) 2, 3, 6 (3和6比较,不需要交换位置) 经过第3次循环,此时剩下参与比较的数据:2, 3 第4次循环: 2, 3 (2和3比较,不需要交换位置)for (int i = 0; i < arr.length - 1; i++) { int m = i; for (int j = i + 1; j < arr.length; j++) { if (arr[m] > arr[j]) { m = j; } } if (m != i) { int temp = arr[m]; arr[m] = arr[i]; arr[i] = temp; } }
选择排序
挑出小的,往前排序,减少交换次数
选择排序比冒泡排序好在:每一次的交换位置都是有意义的
for (int i = 0; i < arr.length; i++) { for (int j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) { int temp = arr[j]; arr[j] = arr[i]; arr[i] = temp; } } }
二分查找
二分法查找算法是基于排序的基础之上。(没有排序的数据是无法查找的。)
二分法查找的终止条件:一直折半,直到中间的那个元素恰好是被查找的元素
public static void main(String[] args) { int[] arr = {3, 5, 7, 9, 12, 56, 78}; int index = FindIndex(arr, 56); System.out.println("找到:" + index); } private static int FindIndex(int[] arr, int i) { if (arr.length == 0) return -1; int begin = 0; int end = arr.length - 1; while (begin <= end) { int midst = (begin + end) / 2;//求出中位数 if (arr[midst] == i) {//判断是否等于要查询的数并返回 return midst; } else if (arr[midst] > i) { //在左侧,已知开始的索引值 end = midst - 1; for (int j = begin; j < end; j++) { if (arr[j] == i) return j; } } else if (arr[midst] < i) { //在左侧,已知结束的索引值 begin = midst + 1; for (int j = begin; j < end; j++) { if (arr[j] == i) return j; } } } return -1; }
String
对 String 在内存存储方面的理解:
字符串一旦创建不可变。
双引号括起来的字符串存储在字符串常量池中。
字符串的比较必须使用 equals 方法。
String 已经重写了 toString()和 equals()方法。
String st=new String("hello"); String st1=new String("hello"); //以上共创建了三对象:两个堆方法区的String对象,方法区字符串常量池一个 System.out.println(st==st1);//false System.out.println(st.equals(st1));//true String st2="hello1"; String st3="hello1"; System.out.println(st2==st3);//true

常用 API
// String类当中常用方法。 //1(掌握).char charAt(int index) char c = "中国人".charAt(1); // "中国人"是一个字符串String对象。只要是对象就能“点.” System.out.println(c); // 国 // 2(了解).int compareTo(String anotherString) // 字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。 int result = "abc".compareTo("abc"); System.out.println(result); //0(等于0) 前后一致 10 - 10 = 0 int result2 = "abcd".compareTo("abce"); System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1 int result3 = "abce".compareTo("abcd"); System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1 // 拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。 System.out.println("xyz".compareTo("yxz")); // -1 // 3(掌握).boolean contains(CharSequence s) // 判断前面的字符串中是否包含后面的子字符串。 System.out.println("HelloWorld.java".contains(".java")); // true System.out.println("http://www.baidu.com".contains("https://")); // false // 4(掌握). boolean endsWith(String suffix) // 判断当前字符串是否以某个子字符串结尾。 System.out.println("test.txt".endsWith(".java")); // false System.out.println("test.txt".endsWith(".txt")); // true System.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true // 5(掌握).boolean equals(Object anObject) // 比较两个字符串必须使用equals方法,不能使用“==” // equals方法有没有调用compareTo方法? 老版本可以看一下。JDK13中并没有调用compareTo()方法。 // equals只能看出相等不相等。 // compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。 System.out.println("abc".equals("abc")); // true // 6(掌握).boolean equalsIgnoreCase(String anotherString) // 判断两个字符串是否相等,并且同时忽略大小写。 System.out.println("ABc".equalsIgnoreCase("abC")); // true // 7(掌握).byte[] getBytes() // 将字符串对象转换成字节数组 byte[] bytes = "abcdef".getBytes(); for(int i = 0; i < bytes.length; i++){ System.out.println(bytes[i]); } // 8(掌握).int indexOf(String str) // 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。 System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); // 6 // 9(掌握).boolean isEmpty() // 判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。 //String s = ""; String s = "a"; System.out.println(s.isEmpty()); // 10(掌握). int length() // 面试题:判断数组长度和判断字符串长度不一样 // 判断数组长度是length属性,判断字符串长度是length()方法。 System.out.println("abc".length()); // 3 System.out.println("".length()); // 0 // 11(掌握).int lastIndexOf(String str) // 判断某个子字符串在当前字符串中最后一次出现的索引(下标) System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22 // 12(掌握). String replace(CharSequence target, CharSequence replacement) // 替换。 // String的父接口就是:CharSequence String 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 // 13(掌握).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]); } String param = "name=zhangsan&password=123&age=20"; String[] params = param.split("&"); for(int i = 0; i <params.length; i++){ System.out.println(params[i]); // 可以继续向下拆分,可以通过“=”拆分。 } // 14(掌握)、boolean startsWith(String prefix) // 判断某个字符串是否以某个子字符串开始。 System.out.println("http://www.baidu.com".startsWith("http")); // true System.out.println("http://www.baidu.com".startsWith("https")); // false // 15(掌握)、 String substring(int beginIndex) 参数是起始下标。 // 截取字符串 System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com // 16(掌握)、String substring(int beginIndex, int endIndex) // beginIndex起始位置(包括) // endIndex结束位置(不包括) System.out.println("http://www.baidu.com".substring(7, 10)); //www // 17(掌握)、char[] toCharArray() // 将字符串转换成char数组 char[] chars = "我是中国人".toCharArray(); for(int i = 0; i < chars.length; i++){ System.out.println(chars[i]); } // 18(掌握)、String toLowerCase() // 转换为小写。 System.out.println("ABCDefKXyz".toLowerCase()); // 19(掌握)、String toUpperCase(); System.out.println("ABCDefKXyz".toUpperCase()); // 20(掌握). String trim(); // 去除字符串前后空白 System.out.println(" hello world ".trim()); // 21(掌握). String中只有一个方法是静态的,不需要new对象 // 这个方法叫做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@10f87f48 System.out.println(s1); //我是一个VIP客户!!!! /* java.lang.StringBuilder StringBuffer和StringBuilder的区别? StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。 StringBuilder中的方法都没有:synchronized关键字修饰,表示StringBuilder在多线程环境下运行是不安全的。 StringBuffer是线程安全的。 StringBuilder是非线程安全的。 */ public class StringBuilderTest01 { public static void main(String[] args) { // 使用StringBuilder也是可以完成字符串的拼接。 StringBuilder sb = new StringBuilder(); sb.append(100); sb.append(true); sb.append("hello"); sb.append("kitty"); System.out.println(sb); } }
八种包装数据类型
Byte
Short
Integer
Integer n=100; Integer m=100; System.out.println(n==m);//true ==>因为Jvm中整数常量池中的数据127~-128 Integer n1=-100; Integer m1=-100; System.out.println(n1==m1);//true Integer n3=128; Integer m3=128; System.out.println(n3==m3);//false Integer n4=-129; Integer m4=-129; System.out.println(n4==m4);//false
Long
Float
Double
Boolean
Character
数字类
DecimalFormat数字格式化 ###,###.## 表示加入千分位,保留两个小数。 ###,###.0000 表示加入千分位,保留4个小数,不够补0 BigDecimal 财务软件中通常使用BigDecimal
异常处理 throws
1、什么是异常,java提供异常处理机制有什么用? 以下程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常 java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常情况, java把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对 程序进行修改,让程序更加的健壮。 什么是异常:程序执行过程中的不正常情况。 异常的作用:增强程序的健壮性。 2、以下程序执行控制台出现了: Exception in thread "main" java.lang.ArithmeticException: / by zero at com.bjpowernode.javase.exception.ExceptionTest01.main(ExceptionTest01.java:14) 这个信息被我们称为:异常信息。这个信息是JVM打印的。处理异常的方式
处理异常的第一种方式: 在方法声明的位置上使用throws关键字抛出,谁调用我这个方法,我就抛给谁。抛给调用者来处理。 这种处理异常的态度:上报。 处理异常的第二种方式: 使用try..catch语句对异常进行捕捉。 这个异常不会上报,自己把这个事儿处理了。 异常抛到此处为止,不再上抛了。 注意: 只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。 另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行。 try..catch捕捉异常之后,后续代码可以执行。 在以后的开发中,处理编译时异常,应该上报还是捕捉呢,怎么选? 如果希望调用者来处理,选择throws上报。 其它情况使用捕捉的方式。 */ public class ExceptionTest06 { // 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。 // 异常处理机制的作用就是增强程序的健壮性。怎么能做到,异常发生了也不影响程序的执行。所以 // 一般main方法中的异常建议使用try..catch进行捕捉。main就不要继续上抛了。 /* public static void main(String[] args) throws FileNotFoundException { System.out.println("main begin"); m1(); System.out.println("main over"); } */ public static void main(String[] args) { // 100 / 0这是算术异常,这个异常是运行时异常,你在编译阶段,可以处理,也可以不处理。编译器不管。 //System.out.println(100 / 0); // 不处理编译器也不管 // 你处理也可以。 /* try { System.out.println(100 / 0); } catch(ArithmeticException e){ System.out.println("算术异常了!!!!"); } */ 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"); } // 抛别的不行,抛ClassCastException说明你还是没有对FileNotFoundException进行处理 //private static void m2() throws ClassCastException{ // 抛FileNotFoundException的父对象IOException,这样是可以的。因为IOException包括FileNotFoundException //private static void m2() throws IOException { // 这样也可以,因为Exception包括所有的异常。 //private static void m2() throws Exception{ // throws后面也可以写多个异常,可以使用逗号隔开。 //private static void m2() throws ClassCastException, FileNotFoundException{ 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 { // 调用SUN jdk中某个类的构造方法。 // 这个类还没有接触过,后期IO流的时候就知道了。 // 我们只是借助这个类学习一下异常处理机制。 // 创建一个输入流对象,该流指向一个文件。 /* 编译报错的原因是什么? 第一:这里调用了一个构造方法: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("如果以上代码出异常,这里会执行吗??????????????????不会!!!"); } }
final finally finalize 有什么区别?
final 关键字 final修饰的类无法继承 final修饰的方法无法覆盖 final修饰的变量不能重新赋值。 finally 关键字 和try一起联合使用。 finally语句块中的代码是必须执行的。 finalize 标识符 是一个Object类中的方法名。 这个方法是由垃圾回收器GC负责调用的。
集合
- ArrayList:底层是数组
- LinkedList:底层是双向链表
- Vector:底层是数组,线程安全的,效率较低,使用较少
- HashSet:底层是 HashMap,放到 HashSet 集合中的元素等同于放到 HashMap 集合中 Key 部分
- TreeSet:底层是 TreeMap,放到 TreeSet 集合中的元素等同于放到 TreeMap 集合中 key 部分
- HashMap:底层是哈希表
- Hashtable:底层是哈希表,线程安全的,效率较低使用较少
- Properties:底层是哈希表,线程安全的,key 和 value 只能存储字符串 String
- TreeMap:底层是二叉树。TreeMap 集合中的 key 可以自动按照大小顺序排序
- List 集合存储元素特点:
- 有序可重复
- 有序:存进去和取出来的顺序相同,每个元素都是有下标的
- 可以重复:存进去
- Set(Map)集合存储元素特点:
- 无序不可以重复
- 无序:存进去和取出来的顺序不一定相同,另外 Set 集合中元素没有下标
- 不可重复:存进去 1,就不能再存进去 1 了
- SortedSet(SortedMap)集合存储元素特点:
- 首先是无序不可重复的,但是 SortedSet 集合中的元素是可排序的
- 无序:存进去和取出来的下标不一定相同,另外 Set 集合没有元素下标
- 不可重复:就不能存入相同的值
- 可排序:可以按照大小顺序排序
- Map 集合中的 Key,就是一个 Set 集合
- 在 Set 集合中放数据,实际上是放在了 Map 集合中的 Key 部分
Collection
package Contains01;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
public class Test01 {
public static void main(String[] args) {
Collection arr = new ArrayList();
arr.add(123);
arr.add(new String("123"));
arr.add("123");
System.out.println(arr.contains("123"));//true
arr.remove("123");
System.out.println(arr.size());//2
User u1 = new User("ZH");
User u2 = new User("ZH");
arr.add(u1);
System.out.println(arr.contains(u2));//在重写equals前为false,重写equals后为true
//contains比较的是内容不是内存地址。remove删除比较的是equals
System.out.println("============");
//创建迭代器迭代对象
Iterator iterator=arr.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
class User {
private String name;
public User() {
}
public User(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
ArrayList
ArrayList集合底层采用了数组这种数据结构
ArrayList集合是非线程安全的
1、ArrayList集合初始化容量是10
2、ArrayList集合底层是Object类型的数组Object[]
3、扩容到原容量的1.5倍
4、建议给定一个预估计的初始化容量,减少数组扩容次数,这是Arraylist集合比较重要的优化策略。
5、数组优点:检索效率高
6、数组缺点:随机增删效率比较低,无法存储大容量数据
7、向数组末尾添加元素,效率很高,不受影响
8、用的比较多,扩容的问题拖慢效率
Link 单向链表

/*
单链表中的节点。
节点是单向链表中基本的单元。
每一个节点Node都有两个属性:
一个属性:是存储的数据。
另一个属性:是下一个节点的内存地址。
*/
public class Node {
// 存储的数据
Object data;
// 下一个节点的内存地址
Node next;
public Node(){
}
public Node(Object data, Node next){
this.data = data;
this.next = next;
}
}
/*
链表类。(单向链表)
*/
public class Link<E> {
public static void main(String[] args) {
Link<String> link = new Link<>();
link.add("abc");
// 类型不匹配。
// link.add(1);
}
// 头节点
Node header;
int size = 0;
public int size(){
return size;
}
// 向链表中添加元素的方法(向末尾添加)
public void add(E data){
//public void add(Object data){
// 创建一个新的节点对象
// 让之前单链表的末尾节点next指向新节点对象。
// 有可能这个元素是第一个,也可能是第二个,也可能是第三个。
if(header == null){
// 说明还没有节点。
// new一个新的节点对象,作为头节点对象。
// 这个时候的头节点既是一个头节点,又是一个末尾节点。
header = new Node(data, null);
}else {
// 说明头不是空!
// 头节点已经存在了!
// 找出当前末尾节点,让当前末尾节点的next是新节点。
Node currentLastNode = findLast(header);
currentLastNode.next = new Node(data, null);
}
size++;
}
/**
* 专门查找末尾节点的方法。
*/
private Node findLast(Node node) {
if(node.next == null) {
// 如果一个节点的next是null
// 说明这个节点就是末尾节点。
return node;
}
// 程序能够到这里说明:node不是末尾节点。
return findLast(node.next); // 递归算法!
}
// 删除链表中某个数据的方法
public void remove(Object obj){
}
// 修改链表中某个数据的方法
public void modify(Object newObj){
}
// 查找链表中某个元素的方法。
public int find(Object obj){
return 1;
}
}
LinkedList
LinkedList集合底层采用了双向链表数据接口
1、LinkedList集合是双向链表
2、对于链表数据结构,随机增删的效率较高,检索效率低
3、链表中的元素,在空间存储上,内存地址不连续

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 是集合工具类。
*/
public class VectorTest {
public static void main(String[] args) {
// 创建一个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");
}
}
泛型机制
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/*
1、JDK5.0之后推出的新特性:泛型
2、泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)
3、使用了泛型好处是什么?
第一:集合中存储的元素类型统一了。
第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”!
4、泛型的缺点是什么?
导致集合中存储的元素缺乏多样性!
大多数业务中,集合中元素的类型还是统一的。所以这种泛型特性被大家所认可。
*/
public class GenericTest01 {
public static void main(String[] args) {
/*
// 不使用泛型机制,分析程序存在缺点
List myList = new ArrayList();
// 准备对象
Cat c = new Cat();
Bird b = new Bird();
// 将对象添加到集合当中
myList.add(c);
myList.add(b);
// 遍历集合,取出每个Animal,让它move
Iterator it = myList.iterator();
while(it.hasNext()) {
// 没有这个语法,通过迭代器取出的就是Object
//Animal a = it.next();
Object obj = it.next();
//obj中没有move方法,无法调用,需要向下转型!
if(obj instanceof Animal){
Animal a = (Animal)obj;
a.move();
}
}
*/
// 使用JDK5之后的泛型机制
// 使用泛型List<Animal>之后,表示List集合中只允许存储Animal类型的数据。
// 用泛型来指定集合中存储的数据类型。
List 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();
}
}
}
}
class Animal {
// 父类自带方法
public void move(){
System.out.println("动物在移动!");
}
}
class Cat extends Animal {
// 特有方法
public void catchMouse(){
System.out.println("猫抓老鼠!");
}
}
class Bird extends Animal {
// 特有方法
public void fly(){
System.out.println("鸟儿在飞翔!");
}
}
自动类型推断机制
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/*
JDK之后引入了:自动类型推断机制。(又称为钻石表达式)
*/
public class GenericTest02 {
public static void main(String[] args) {
// ArrayList<这里的类型会自动推断>(),前提是JDK8之后才允许。
// 自动类型推断,钻石表达式!
List<Animal> myList = new ArrayList<>();
myList.add(new Animal());
myList.add(new Cat());
myList.add(new Bird());
// 遍历
Iterator<Animal> it = myList.iterator();
while(it.hasNext()){
Animal a = it.next();
a.move();
}
List<String> strList = new ArrayList<>();
// 类型不匹配。
//strList.add(new Cat());
strList.add("http://www.126.com");
strList.add("http://www.baidu.com");
strList.add("http://www.bjpowernode.com");
// 类型不匹配。
//strList.add(123);
//System.out.println(strList.size());
// 遍历
Iterator<String> it2 = strList.iterator();
while(it2.hasNext()){
// 如果没有使用泛型
/*
Object obj = it2.next();
if(obj instanceof String){
String ss = (String)obj;
ss.substring(7);
}
*/
// 直接通过迭代器获取了String类型的数据
String s = it2.next();
// 直接调用String类的substring方法截取字符串。
String newString = s.substring(7);
System.out.println(newString);
}
}
}
自定义泛型
/*
自定义泛型可以吗?可以
自定义泛型的时候,<> 尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:
<E>和<T>
E是Element单词首字母。
T是Type单词首字母。
*/
public class GenericTest03<标识符随便写> {
public void doSome(标识符随便写 o){
System.out.println(o);
}
public static void main(String[] args) {
// new对象的时候指定了泛型是:String类型
GenericTest03<String> gt = new GenericTest03<>();
// 类型不匹配
//gt.doSome(100);
gt.doSome("abc");
// =============================================================
GenericTest03<Integer> gt2 = new GenericTest03<>();
gt2.doSome(100);
// 类型不匹配
//gt2.doSome("abc");
MyIterator<String> mi = new MyIterator<>();
String s1 = mi.get();
MyIterator<Animal> mi2 = new MyIterator<>();
Animal a = mi2.get();
// 不用泛型就是Object类型。
/*GenericTest03 gt3 = new GenericTest03();
gt3.doSome(new Object());*/
}
}
class MyIterator<T> {
public T get(){
return null;
}
}
Foreach
/*
JDK5.0之后推出了一个新特性:叫做增强for循环,或者叫做foreach
*/
public class ForEachTest01 {
public static void main(String[] args) {
// int类型数组
int[] arr = {432,4,65,46,54,76,54};
// 遍历数组(普通for循环)
for(int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 增强for(foreach)
// 以下是语法
/*for(元素类型 变量名 : 数组或集合){
System.out.println(变量名);
}*/
System.out.println("======================================");
// foreach有一个缺点:没有下标。在需要使用下标的循环中,不建议使用增强for循环。
for(int data : arr) {
// data就是数组中的元素(数组中的每一个元素。)
System.out.println(data);
}
}
}
/*
集合使用foreach
*/
public class ForEachTest02 {
public static void main(String[] args) {
// 创建List集合
List<String> strList = new ArrayList<>();
// 添加元素
strList.add("hello");
strList.add("world!");
strList.add("kitty!");
// 遍历,使用迭代器方式
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String s = it.next();
System.out.println(s);
}
// 使用下标方式(只针对于有下标的集合)
for(int i = 0; i < strList.size(); i++){
System.out.println(strList.get(i));
}
// 使用foreach
for(String s : strList){ // 因为泛型使用的是String类型,所以是:String s
System.out.println(s);
}
List<Integer> list = new ArrayList<>();
list.add(100);
list.add(200);
list.add(300);
for(Integer i : list){ // i代表集合中的元素
System.out.println(i);
}
}
}
HashSet
import java.util.Set;
import java.util.TreeSet;
/*
TreeSet集合存储元素特点:
1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
称为:可排序集合。
2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
*/
public class TreeSetTest01 {
public static void main(String[] args) {
// 创建集合对象
Set<String> strs = new TreeSet<>();
// 添加元素
strs.add("A");
strs.add("B");
strs.add("Z");
strs.add("Y");
strs.add("Z");
strs.add("K");
strs.add("M");
// 遍历
/*
A
B
K
M
Y
Z
从小到大自动排序!
*/
for(String s : strs){
System.out.println(s);
}
}
}
TreeSet
import java.util.Set;
import java.util.TreeSet;
/*
TreeSet集合存储元素特点:
1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
称为:可排序集合。
2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
*/
public class TreeSetTest01 {
public static void main(String[] args) {
// 创建集合对象
Set<String> strs = new TreeSet<>();
// 添加元素
strs.add("A");
strs.add("B");
strs.add("Z");
strs.add("Y");
strs.add("Z");
strs.add("K");
strs.add("M");
// 遍历
/*
A
B
K
M
Y
Z
从小到大自动排序!
*/
for(String s : strs){
System.out.println(s);
}
}
}
Map
常用接口
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/*
java.util.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<V> values() 获取Map集合中所有的value,返回一个Collection
Set<K> 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
*/
public class MapTest01 {
public static void main(String[] args) {
// 创建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获取value
String value = map.get(2);
System.out.println(value);
// 获取键值对的数量
System.out.println("键值对的数量:" + map.size());
// 通过key删除key-value
map.remove(2);
System.out.println("键值对的数量:" + map.size());
// 判断是否包含某个key
// contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。
System.out.println(map.containsKey(new Integer(4))); // true
// 判断是否包含某个value
System.out.println(map.containsValue(new String("wangwu"))); // true
// 获取所有的value
Collection<String> values = map.values();
// foreach
for(String s : values){
System.out.println(s);
}
// 清空map集合
map.clear();
System.out.println("键值对的数量:" + map.size());
// 判断是否为空
System.out.println(map.isEmpty()); // true
}
}
遍历
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/*
Map集合的遍历。【非常重要】
*/
public class MapTest02 {
public static void main(String[] args) {
// 第一种方式:获取所有的key,通过遍历key,来遍历value
Map<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()){
// 取出其中一个key
Integer key = it.next();
// 通过key获取value
String 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.Entry
Set<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
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/*
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 class HashMapTest01 {
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());
}
}
哈希表存储数据的特点

import java.util.HashSet;
import java.util.Set;
/*
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值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。
*/
public class HashMapTest02 {
public static void main(String[] args) {
Student s1 = new Student("zhangsan");
Student s2 = new Student("zhangsan");
// 重写equals方法之前是false
//System.out.println(s1.equals(s2)); // false
// 重写equals方法之后是true
System.out.println(s1.equals(s2)); //true (s1和s2表示相等)
System.out.println("s1的hashCode=" + s1.hashCode()); //284720968 (重写hashCode之后-1432604525)
System.out.println("s2的hashCode=" + s2.hashCode()); //122883338 (重写hashCode之后-1432604525)
// s1.equals(s2)结果已经是true了,表示s1和s2是一样的,相同的,那么往HashSet集合中放的话,
// 按说只能放进去1个。(HashSet集合特点:无序不可重复)
Set<Student> students = new HashSet<>();
students.add(s1);
students.add(s2);
System.out.println(students.size()); // 这个结果按说应该是1. 但是结果是2.显然不符合HashSet集合存储特点。怎么办?
}
}
import java.util.HashMap;
import java.util.Map;
/*
HashMap集合key部分允许null吗?
允许
但是要注意:HashMap集合的key null值只能有一个。
有可能面试的时候遇到这样的问题。
*/
public class HashMapTest03 {
public static void main(String[] args) {
Map map = new HashMap();
// HashMap集合允许key为null
map.put(null, null);
System.out.println(map.size()); // 1
// key重复的话value是覆盖!
map.put(null, 100);
System.out.println(map.size()); //1
// 通过key获取value
System.out.println(map.get(null)); // 100
}
}
Hashtable
import java.util.Hashtable;
import java.util.Map;
/*
Hashtable的key可以为null吗?
Hashtable的key和value都是不能为null的。
HashMap集合的key和value都是可以为null的。
Hashtable方法都带有synchronized:线程安全的。
线程安全有其它的方案,这个Hashtable对线程的处理
导致效率较低,使用较少了。
Hashtable和HashMap一样,底层都是哈希表数据结构。
Hashtable的初始化容量是11,默认加载因子是:0.75f
Hashtable的扩容是:原容量 * 2 + 1
*/
public class HashtableTest01 {
public static void main(String[] args) {
Map map = new Hashtable();
//map.put(null, "123");
map.put(100, null);
}
}
Properties
import java.util.Properties;
/*
目前只需要掌握Properties属性类对象的相关方法即可。
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。
*/
public class PropertiesTest01 {
public static void main(String[] args) {
// 创建一个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获取value
String 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);
}
}
TreeSet
存储元素特点
/*
TreeSet集合存储元素特点:
1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
称为:可排序集合。
2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
*/
public class TreeSetTest01 {
public static void main(String[] args) {
// 创建集合对象
Set<String> strs = new TreeSet<>();
// 添加元素
strs.add("A");
strs.add("B");
strs.add("Z");
strs.add("Y");
strs.add("Z");
strs.add("K");
strs.add("M");
// 遍历
/*
A
B
K
M
Y
Z
从小到大自动排序!
*/
for(String s : strs){
System.out.println(s);
}
}
}
TreeSet 底层是 TreeMap
/*
1、TreeSet集合底层实际上是一个TreeMap
2、TreeMap集合底层是一个二叉树。
3、放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。
4、TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。
称为:可排序集合。
*/
public class TreeSetTest02 {
public static void main(String[] args) {
// 创建一个TreeSet集合
TreeSet<String> ts = new TreeSet<>();
// 添加String
ts.add("zhangsan");
ts.add("lisi");
ts.add("wangwu");
ts.add("zhangsi");
ts.add("wangliu");
// 遍历
for(String s : ts){
// 按照字典顺序,升序!
System.out.println(s);
}
TreeSet<Integer> ts2 = new TreeSet<>();
ts2.add(100);
ts2.add(200);
ts2.add(900);
ts2.add(800);
ts2.add(600);
ts2.add(10);
for(Integer elt : ts2){
// 升序!
System.out.println(elt);
}
}
}
/*
数据库中有很多数据:
userid name birth
-------------------------------------
1 zs 1980-11-11
2 ls 1980-10-11
3 ww 1981-11-11
4 zl 1979-11-11
编写程序从数据库当中取出数据,在页面展示用户信息的时候按照生日升序或者降序。
这个时候可以使用TreeSet集合,因为TreeSet集合放进去,拿出来就是有顺序的。
*/
自定义排序
import java.util.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
// 比较规则最终还是由程序员指定的:例如按照年龄升序。或者按照年龄降序。
@Override
public 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 <0
return c.age - this.age;
}
public String toString() {
return "Customer[age=" + age + "]";
}
}
//编写测试
import java.util.TreeSet;
/*
先按照年龄升序,如果年龄一样的再按照姓名升序。
*/
public class TreeSetTest05 {
public static void main(String[] args) {
TreeSet<Vip> vips = new TreeSet<>();
vips.add(new Vip("zhangsi", 20));
vips.add(new Vip("zhangsan", 20));
vips.add(new Vip("king", 18));
vips.add(new Vip("soft", 17));
for(Vip vip : vips){
System.out.println(vip);
}
}
}
class Vip implements Comparable<Vip>{
String name;
int age;
public Vip(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Vip{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
/*
compareTo方法的返回值很重要:
返回0表示相同,value会覆盖。
返回>0,会继续在右子树上找。【10 - 9 = 1 ,1 > 0的说明左边这个数字比较大。所以在右子树上找。】
返回<0,会继续在左子树上找。
*/
@Override
public int compareTo(Vip v) {
// 写排序规则,按照什么进行比较。
if(this.age == v.age){
// 年龄相同时按照名字排序。
// 姓名是String类型,可以直接比。调用compareTo来完成比较。
return this.name.compareTo(v.name);
} else {
// 年龄不一样
return this.age - v.age;
}
}
}
Comparable 和 Comparator
import java.util.Comparator;
import java.util.TreeSet;
/*
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>() {
@Override
public 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;
}
@Override
public String toString() {
return "小乌龟[" +
"age=" + age +
']';
}
}
// 单独在这里编写一个比较器
// 比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。)
/*
class WuGuiComparator implements Comparator<WuGui> {
@Override
public int compare(WuGui o1, WuGui o2) {
// 指定比较规则
// 按照年龄排序
return o1.age - o2.age;
}
}
*/
二叉树

Collections 工具类
import java.util.*;
/*
java.util.Collection 集合接口
java.util.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;
}
@Override
public int compareTo(WuGui2 o) {
return this.age - o.age;
}
@Override
public String toString() {
return "WuGui2{" +
"age=" + age +
'}';
}
}
IO
- I : Input
- O : Output
- 通过 IO 可以完成硬盘文件的读和写。

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 流四个主要类
- 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 中只要“类名”以 Stream 结尾的都是字节流。以“Reader/Writer”结尾的都是字符流。
FileInputStream
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*
最终版,需要掌握。
*/
public class FileInputStreamTest04 {
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();
}
}
}
}
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*
FileInputStream类的其它常用方法:
int available():返回流当中剩余的没有读到的字节数量
long skip(long n):跳过几个字节不读。
*/
public class FileInputStreamTest05 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
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
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileOutputStream
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 文件字节输出流,负责写。
* 从内存到硬盘。
*/
public class FileOutputStreamTest01 {
public static void main(String[] args) {
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();
}
}
}
}
}
FileInputStream 文件拷贝
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class copy01 {
/*
使用FileInputStream + FileOutputStream完成文件的拷贝。
拷贝的过程应该是一边读,一边写。
使用以上的字节流拷贝文件的时候,文件类型随意,万能的。什么样的文件都能拷贝。
*/
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 创建一个输入流对象
fis = new FileInputStream("C:\\Users\\ita20\\Desktop\\demo\\demo.css");
// 创建一个输出流对象
fos = new FileOutputStream("C:\\Users\\ita20\\Desktop\\demo.css");
// 最核心的:一边读,一边写
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
/*
FileReader:
文件字符输入流,只能读取普通文本。
读取文本内容时,比较方便,快捷。
*/
public class FileReaderTest {
public static void main(String[] args) {
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();
}
}
}
}
}
FileReader 文件拷贝
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/*
使用FileReader FileWriter进行拷贝的话,只能拷贝“普通文本”文件。
*/
public class Copy02 {
public static void main(String[] args) {
FileReader in = null;
FileWriter out = null;
try {
// 读
in = new FileReader("chapter23/src/com/bjpowernode/java/io/Copy02.java");
// 写
out = new FileWriter("C111111.java");
// 一边读一边写:
char[] chars = new char[1024 * 512]; // 1MB
int 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();
}
}
}
}
}
BufferedReader
import java.io.BufferedReader;
import java.io.FileReader;
/*
BufferedReader:
带有缓冲区的字符输入流。
使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组。自带缓冲。
*/
public class BufferedReaderTest01 {
public static void main(String[] args) throws Exception{
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
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
/*
转换流:InputStreamReader
*/
public class BufferedReaderTest02 {
public static void main(String[] args) throws Exception{
/*// 字节流
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
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
/*
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.js", true)));
// 开始写。
out.write("hello world!");
out.write("\n");
out.write("hello kitty!");
// 刷新
out.flush();
// 关闭最外层
out.close();
}
}
DataInputStream(读)
import java.io.DataInputStream;
import java.io.FileInputStream;
/*
DataInputStream:数据字节输入流。
DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候你需要提前知道写入的顺序。
读的顺序需要和写的顺序一致。才可以正常取出数据。
*/
public class DataInputStreamTest01 {
public static void main(String[] args) throws Exception{
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(写)
import java.io.DataOutputStream;
import java.io.FileOutputStream;
/*
java.io.DataOutputStream:数据专属的流。
这个流可以将数据连同数据的类型一并写入文件。
注意:这个文件不是普通文本文档。(这个文件使用记事本打不开。)
*/
public class DataOutputStreamTest {
public static void main(String[] args) throws Exception{
// 创建数据专属的字节输出流
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
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public class LogPrint {
public static void log(String msg) {
try {
// 指向一个日志文件 将文件对象转换
PrintStream printStream = new PrintStream(new FileOutputStream("C:\\Users\\ita20\\Desktop\\demo\\log.txt", true));
//改变输出方向
System.setOut(printStream);
//获取当前时间
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String dtime = simpleDateFormat.format(date);
//将日志输入
System.out.println(dtime + ":" + msg);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
File
/*
File
1、File类和四大家族没有关系,所以File类不能完成文件的读和写。
2、File对象代表什么?
文件和目录路径名的抽象表示形式。
C:\Drivers 这是一个File对象
C:\Drivers\Lan\Realtek\Readme.txt 也是File对象。
一个File对象有可能对应的是目录,也可能是文件。
File只是一个路径名的抽象表示形式。
3、需要掌握File类中常用的方法
*/
public class FileTest01 {
public static void main(String[] args) throws Exception {
// 创建一个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\copy
}
}
/*
File类的常用方法
*/
public class FileTest02 {
public static void main(String[] args) {
File 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字节。
}
}
拷贝文件目录
public class Test01 {
public static void main(String[] args) {
// File file = new File("E:\\002-JavaSE每章作业");
//
// for (File fs : file.listFiles()) {
// System.out.println(fs.getName());
// }
Readwrite(new File("C:\\Users\\ita20\\Desktop\\被拷贝文件\\"), new File("C:\\Users\\ita20\\Desktop\\拷贝到此处\\"));
}
public static void Readwrite(File filePath, File nfilePaath) {
if (filePath.isFile()) { //是文件
FileOutputStream fout = null;//写
FileInputStream finp = null;//读
try {
//实例需要拷贝的路径
finp = new FileInputStream(filePath);
String path = nfilePaath.getAbsolutePath() + "\\" + filePath.getName();
fout = new FileOutputStream(path);
byte[] bytes = new byte[1024 * 1024];
int i = 0;//每次拷贝的字节数
while ((i = finp.read(bytes)) != -1) {
//写入到文件中
fout.write(bytes);
}
fout.flush();//写入通道每次刷新
} catch (IOException e) {
e.printStackTrace();
} finally {
if (finp == null) {
try {
finp.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fout == null) {
try {
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return;
}
File[] fs = filePath.listFiles();
for (File file : fs) {
if (file.isDirectory()) {
String path = nfilePaath.getAbsolutePath() + "\\" + file.getName();
File ff = new File(path);
if (!ff.exists()) {
ff.mkdirs();
Readwrite(file, ff);
System.out.println(ff);
}
} else {
Readwrite(file, nfilePaath);
}
}
}
/**
* 拷贝目录
*
* @param srcFile 拷贝源
* @param destFile 拷贝目标
*/
private static void copyDir(File srcFile, File destFile) {
if (srcFile.isFile()) {
// srcFile如果是一个文件的话,递归结束。
// 是文件的时候需要拷贝。
// ....一边读一边写。
FileInputStream in = null;
FileOutputStream out = null;
try {
// 读这个文件
// D:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdf
in = new FileInputStream(srcFile);
// 写到这个文件中
// C:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdf
String path = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcFile.getAbsolutePath().substring(3);
out = new FileOutputStream(path);
// 一边读一边写
byte[] bytes = new byte[1024 * 1024]; // 一次复制1MB
int readCount = 0;
while ((readCount = in.read(bytes)) != -1) {
out.write(bytes, 0, readCount);
}
out.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return;
}
// 获取源下面的子目录
File[] files = srcFile.listFiles();
for (File file : files) {
// 获取所有文件的(包括目录和文件)绝对路径
//System.out.println(file.getAbsolutePath());
if (file.isDirectory()) {
// 新建对应的目录
//System.out.println(file.getAbsolutePath());
//D:\course\02-JavaSE\document\JavaSE进阶讲义 源目录
//C:\course\02-JavaSE\document\JavaSE进阶讲义 目标目录
String srcDir = file.getAbsolutePath();
String destDir = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcDir.substring(3);
File newFile = new File(destDir);
if (!newFile.exists()) {
newFile.mkdirs();
System.out.println(newFile);
}
}
// 递归调用
System.out.println(destFile + ",,,,destFile");
copyDir(file, destFile);
}
}
}
序列化\反序列化
import java.io.Serializable;
public class Student implements Serializable {
// IDEA工具自动生成序列化版本号。
//private static final long serialVersionUID = -7998917368642754840L;
// Java虚拟机看到Serializable接口之后,会自动生成一个序列化版本号。
// 这里没有手动写出来,java虚拟机会默认提供这个序列化版本号。
// 建议将序列化版本号手动的写出来。不建议自动生成
private static final long serialVersionUID = 1L; // java虚拟机识别一个类的时候先通过类名,如果类名一致,再通过序列化版本号。
private int no;
//private String name;
// 过了很久,Student这个类源代码改动了。
// 源代码改动之后,需要重新编译,编译之后生成了全新的字节码文件。
// 并且class文件再次运行的时候,java虚拟机生成的序列化版本号也会发生相应的改变。
private int age;
private String email;
// transient关键字表示游离的,不参与序列化。
private transient String address;//address不参与序列化操作
public Student() {
}
public Student(int no, String name) {
this.no = no;
//this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
/*public String getName() {
return name;
}*/
/*public void setName(String name) {
this.name = name;
}*/
/*@Override
public String toString() {
return "Student{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}*/
@Override
public String toString() {
return "Student{" +
"no=" + no +
", age=" + age +
", email='" + email + '\'' +
", address='" + address + '\'' +
'}';
}
}
序列化写入
/*
一次序列化多个对象呢?
可以,可以将对象放到集合当中,序列化集合。
提示:
参与序列化的ArrayList集合以及集合中的元素User都需要实现 java.io.Serializable接口。
*/
public class ObjectOutputStreamTest02 {
public static void main(String[] args) throws Exception{
List<Student> userList = new ArrayList<>();
userList.add(new Student(1,"zhangsan"));
userList.add(new Student(2, "lisi"));
userList.add(new Student(3, "wangwu"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Student"));
// 序列化一个集合,这个集合对象中放了很多其他对象。
oos.writeObject(userList);
oos.flush();
oos.close();
}
}
反序列化读取
/*
反序列化集合
*/
public class ObjectInputStreamTest02 {
public static void main(String[] args) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Student"));
//Object obj = ois.readObject();
//System.out.println(obj instanceof List);
List<Student> userList = (List<Student>)ois.readObject();
for(Student user : userList){
System.out.println(user);
}
ois.close();
}
}
I0+Properties
import java.io.FileReader;
import java.util.Properties;
/*
IO+Properties的联合应用。
非常好的一个设计理念:
以后经常改变的数据,可以单独写到一个文件中,使用程序动态读取。
将来只需要修改这个文件的内容,java代码不需要改动,不需要重新
编译,服务器也不需要重启。就可以拿到动态的信息。
类似于以上机制的这种文件被称为配置文件。
并且当配置文件中的内容格式是:
key1=value
key2=value
的时候,我们把这种配置文件叫做属性配置文件。
java规范中有要求:属性配置文件建议以.properties结尾,但这不是必须的。
这种以.properties结尾的文件在java中被称为:属性配置文件。
其中Properties是专门存放属性配置文件内容的一个类。
*/
public class IoPropertiesTest01 {
public static void main(String[] args) throws Exception{
/*
Properties是一个Map集合,key和value都是String类型。
想将userinfo文件中的数据加载到Properties对象当中。
*/
// 新建一个输入流对象
FileReader reader = new FileReader("chapter23/userinfo.properties");
// 新建一个Map集合
Properties pro = new Properties();
// 调用Properties对象的load方法将文件中的数据加载到Map集合中。
pro.load(reader); // 文件中的数据顺着管道加载到Map集合中,其中等号=左边做key,右边做value
// 通过key来获取value呢?
String username = pro.getProperty("username");
System.out.println(username);
String password = pro.getProperty("password");
System.out.println(password);
String data = pro.getProperty("data");
System.out.println(data);
String usernamex = pro.getProperty("usernamex");
System.out.println(usernamex);
}
}
*流
文件专属: java.io.FileInputStream(掌握) java.io.FileOutputStream(掌握) java.io.FileReader java.io.FileWriter 转换流:(将字节流转换成字符流) java.io.InputStreamReader java.io.OutputStreamWriter
缓冲流专属: java.io.BufferedReader java.io.BufferedWriter java.io.BufferedInputStream java.io.BufferedOutputStream
数据流专属: java.io.DataInputStream java.io.DataOutputStream
标准输出流: java.io.PrintWriter java.io.PrintStream(掌握)
对象专属流: java.io.ObjectInputStream(掌握) java.io.ObjectOutputStream(掌握)
Thread
什么是进程?什么是线程?
进程是一个应用程序(1 个进程是一个软件)。
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。
对于 java 程序来说,当在 DOS 命令窗口中输入:
- java HelloWorld 回车之后。
- 会先启动 JVM,而 JVM 就是一个进程。
- JVM 再启动一个主线程调用 main 方法。
- 同时再启动一个垃圾回收线程负责看护,回收垃圾。
- 最起码,现在的 java 程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行 main 方法的主线程。
举个例子
阿里巴巴:进程 马云:阿里巴巴的一个线程 童文红:阿里巴巴的一个线程 京东:进程 强东:京东的一个线程 妹妹:京东的一个线程 进程可以看做是现实生活当中的公司。 线程可以看做是公司当中的某个员工。 注意: 进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!) 魔兽游戏是一个进程 酷狗音乐是一个进程 这两个进程是独立的,不共享资源。 线程A和线程B呢? 在java语言中: 线程A和线程B,堆内存和方法区内存共享。 但是栈内存独立,一个线程一个栈。 假设启动10个线程,会有10个栈空间,每个栈和每个栈之间, 互不干扰,各自执行各自的,这就是多线程并发。 火车站,可以看做是一个进程。 火车站中的每一个售票窗口可以看做是一个线程。 我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。 所以多线程并发可以提高效率。 java中之所以有多线程机制,目的就是为了提高程序的处理效率。
使用了多线程机制之后,main 方法结束,是不是有可能程序也不会结束。main 方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。
分析一个问题:对于单核的 CPU 来说,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没问题的。 4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
什么是真正的多线程并发? t1线程执行t1的。 t2线程执行t2的。 t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。
单核的CPU表示只有一个大脑: 不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。 对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于 CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情 同时在做!!!!! 线程A:播放音乐 线程B:运行魔兽游戏 线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行, 给我们的感觉是同时并发的。
电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后, 人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像 一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的”时间的,在 这个期间计算机可以进行亿万次的循环。所以计算机的执行速度很快。
一个线程一个栈
- 进程中方法区和堆区是公用的

实现线程的两种方式
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();
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承
其它的类,更灵活。
Start/Run
Run

Start

线程的生命周期

getName
获得线程名字
/* 1、怎么获取当前线程对象? Thread t = Thread.currentThread(); 返回值t就是当前线程。 2、获取线程对象的名字 String name = 线程对象.getName(); 3、修改线程对象的名字 线程对象.setName("线程名字"); 4、当线程没有设置名字的时候,默认的名字有什么规律?(了解一下) Thread-0 Thread-1 Thread-2 Thread-3 ..... */ public class ThreadTest05 { public void doSome(){ // 这样就不行了 //this.getName(); //super.getName(); // 但是这样可以 String name = Thread.currentThread().getName(); System.out.println("------->" + name); } public static void main(String[] args) { ThreadTest05 tt = new ThreadTest05(); tt.doSome(); //currentThread就是当前线程对象 // 这个代码出现在main方法当中,所以当前线程就是主线程。 Thread currentThread = Thread.currentThread(); System.out.println(currentThread.getName()); //main // 创建线程对象 MyThread2 t = new MyThread2(); // 设置线程的名字 t.setName("t1"); // 获取线程的名字 String tName = t.getName(); System.out.println(tName); //Thread-0 MyThread2 t2 = new MyThread2(); t2.setName("t2"); System.out.println(t2.getName()); //Thread-1\ t2.start(); // 启动线程 t.start(); } } class MyThread2 extends Thread { public void run(){ for(int i = 0; i < 100; i++){ // currentThread就是当前线程对象。当前线程是谁呢? // 当t1线程执行run方法,那么这个当前线程就是t1 // 当t2线程执行run方法,那么这个当前线程就是t2 Thread currentThread = Thread.currentThread(); System.out.println(currentThread.getName() + "-->" + i); //System.out.println(super.getName() + "-->" + i); //System.out.println(this.getName() + "-->" + i); } } }
Sleep
/* 关于线程的sleep方法: static void sleep(long millis) 1、静态方法:Thread.sleep(1000); 2、参数是毫秒 3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。 这行代码出现在A线程中,A线程就会进入休眠。 这行代码出现在B线程中,B线程就会进入休眠。 4、Thread.sleep()方法,可以做到这种效果: 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。 */ public class ThreadTest06 { public static void main(String[] args) { // 让当前线程进入休眠,睡眠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(); } } } }Sleep 面试题
Sleep 是静态方法,和对象无关
/* 关于Thread.sleep()方法的一个面试题: */ 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); } } }
中止睡眠的线程
interrupt/* 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()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。 @Override public 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{ }*/ }
Stop
这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了
/* 在java中怎么强行终止一个线程的执行。 这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了, 线程没有保存的数据将会丢失。不建议使用。 */ public class ThreadTest09 { public static void main(String[] args) { Thread t = new Thread(new MyRunnable3()); t.setName("t"); t.start(); // 模拟5秒 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } // 5秒之后强行终止t线程 t.stop(); // 已过时(不建议使用。) } } class MyRunnable3 implements Runnable { @Override public void run() { for(int i = 0; i < 10; i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
终止线程
给定一个条件去判断是否执行线程,若为 false 则直接 retuen 终止 run 方法结束线程
/* 怎么合理的终止一个线程的执行。这种方式是很常用的。 */ 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; @Override public 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; } } } }
线程优先级 setPriority
线程优先级越高,抢占的 CPU 时间片就相对更多,几率更大。
/* 了解:关于线程的优先级 */ public class ThreadTest11 { public static void main(String[] args) { // 设置主线程的优先级为1 Thread.currentThread().setPriority(1); /*System.out.println("最高优先级" + Thread.MAX_PRIORITY); System.out.println("最低优先级" + Thread.MIN_PRIORITY); System.out.println("默认优先级" + Thread.NORM_PRIORITY);*/ // 获取当前线程对象,获取当前线程的优先级 Thread currentThread = Thread.currentThread(); // main线程的默认优先级是:5 //System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority()); Thread t = new Thread(new MyRunnable5()); t.setPriority(10); t.setName("t"); t.start(); // 优先级较高的,只是抢到的CPU时间片相对多一些。 // 大概率方向更偏向于优先级比较高的。 for(int i = 0; i < 10000; i++){ System.out.println(Thread.currentThread().getName() + "-->" + i); } } } class MyRunnable5 implements Runnable { @Override public void run() { // 获取线程优先级 //System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority()); for(int i = 0; i < 10000; i++){ System.out.println(Thread.currentThread().getName() + "-->" + i); } } }xia
线程让位 yield
给点一定的条件让出线程的执行权
/* 让位,当前线程暂停,回到就绪状态,让给其它线程。 静态方法:Thread.yield(); */ public class ThreadTest12 { public static void main(String[] args) { Thread t = new Thread(new MyRunnable6()); t.setName("t"); t.start(); for(int i = 1; i <= 10000; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); } } } class MyRunnable6 implements Runnable { @Override public void run() { for(int i = 1; i <= 10000; i++) { //每100个让位一次。 if(i % 100 == 0){ Thread.yield(); // 当前线程暂停一下,让给主线程。 } System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
线程合并 join
合并线程后,当前的线程会受到阻塞,直至合并过来的线程执行结束。
/* 线程合并 */ public class ThreadTest13 { public static void main(String[] args) { System.out.println("main begin"); Thread t = new Thread(new MyRunnable7()); t.setName("t"); t.start(); //合并线程 try { t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main over"); } } class MyRunnable7 implements Runnable { @Override public void run() { for(int i = 0; i < 10000; i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
关于多线程并发环境下数据的安全问题
为什么这个是重点?
- 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
- 最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*)
什么时候数据在多线程并发的环境下会存在安全问题呢?
- 三个条件: 条件 1:多线程并发。 条件 2:有共享数据。 条件 3:共享数据有修改的行为。
- 满足以上 3 个条件之后,就会存在线程安全问题。
怎么解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
- 线程排队执行。(不能并发)
- 用排队执行解决线程安全问题。
- 这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
怎么解决线程安全问题呀?
- 使用“线程同步机制”。
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
说到线程同步这块,涉及到这两个专业术语
- 异步编程模型:
- 线程 t1 和线程 t2,各自执行各自的,t1 不管 t2,t2 不管 t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高)异步就是并发。
- 同步编程模型:
- 线程 t1 和线程 t2,在线程 t1 执行的时候,必须等待 t2 线程执行结束,或者说在 t2 线程执行的时候,必须等待 t1 线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。同步就是排队。
- 异步编程模型:
Java 中三大变量
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
- 局部变量永远都不会存在线程安全问题。
- 因为局部变量不共享。(一个线程一个栈。)
- 局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有 1 个。
静态变量在方法区中,方法区只有 1 个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
如果使用局部变量的话:
- 建议使用:StringBuilder。
- 因为局部变量不存在线程安全问题。选择 StringBuilder。
- StringBuffer 效率比较低。
- ArrayList 是非线程安全的。
- Vector 是线程安全的。
- HashMap HashSet 是非线程安全的。
- Hashtable 是线程安全的。
Synchronized
第一种:同步代码块 //灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized 表示共享对象一定是this 并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized 表示找类锁。 类锁永远只有1把。 就算创建了100个对象,那类锁也只有一把。
对象锁:1个对象1把锁,100个对象100把锁。 类锁:100个对象,也可能只是1把类锁。
银行取钱问题
public class Account { // 账号 private String actno; // 余额 private double balance; 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; } //取款的方法 /* 在实例方法上可以使用synchronized吗?可以的。 synchronized出现在实例方法上,一定锁的是this。 没得挑。只能是this。不能是其他的对象了。 所以这种方式不灵活。 另外还有一个缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。 synchronized使用在实例方法上有什么优点? 代码写的少了。节俭了。 如果共享的对象就是this,并且需要同步的代码块是整个方法体, 建议使用这种方式。 */ public synchronized void withdraw(double money){ double before = this.getBalance(); // 10000 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方法的执行表示取款操作。 // 假设取款5000 double money = 5000; // 取款 // 多线程并发执行这个方法。 act.withdraw(money); System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance()); } } public class Test { public static void main(String[] args) { // 创建账户对象(只创建1个) Account act = new Account("act-001", 10000); // 创建两个线程 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }
Synchronized 面试题
// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗? //不需要,因为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"); 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方法的结束吗? //需要 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"); try { Thread.sleep(1000 * 10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("doSome over"); } public synchronized 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 { 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 void doOther(){ System.out.println("doOther begin"); System.out.println("doOther over"); } }// 面试题:doOther方法执行的时候需要等待doSome方法的结束吗? //需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。 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 { // synchronized出现在静态方法上是找类锁。 public synchronized static 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(){ System.out.println("doOther begin"); System.out.println("doOther over"); } }
死锁
package com.bjpowernode.java.deadlock;
/*
死锁代码要会写。
一般面试官要求你会写。
只有会写的,才会在以后的开发中注意这个事儿。
因为死锁很难调试。
*/
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
Thread 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){
}
}
}
}
开发中应该怎么解决线程安全问题
- 是一上来就选择线程同步吗?synchronized 不是,synchronized 会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应 1 个对象,100 个线程对应 100 个对象,对象不共享,就没有数据安全问题了。)第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择 synchronized 了。线程同步机制。
守护线程
java语言中线程分为两大类: 一类是:用户线程 一类是:守护线程(后台线程) 其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点: 一般守护线程是一个死循环,所有的用户线程只要结束, 守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢? 每天00:00的时候系统数据自动备份。 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。 一直在那里看着,没到00:00的时候就备份一次。所有的用户线程 如果结束了,守护线程自动退出,没有必要进行数据备份了。 // 启动线程之前,将线程设置为守护线程 t.setDaemon(true);
/*
守护线程
*/
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run(){
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
定时器
作用:间隔特定的时间,执行特定的程序。
每周要进行银行账户的总账操作。 每天要进行数据的备份操作。
在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的, 那么在java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行 任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。 不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持 定时任务的。
在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架, 这个框架只要进行简单的配置,就可以完成定时器的任务。
public static void main(String[] args) throws ParseException {
SimpleDateFormat sdt=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date dt=sdt.parse("2020-11-25 10:14:10");
System.out.println(dt);
Timer timer=new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("123"+dt);
}
}, dt, 1000 * 3);
}
实现线程的第三种方式
实现线程的第三种方式:实现Callable接口。(JDK8新特性。) 这种方式实现的线程可以获取线程的返回值。 之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。 思考: 系统委派一个线程去执行一个任务,该线程执行完任务之后,可能 会有一个执行结果,我们怎么能拿到这个执行结果呢? 使用第三种方式:实现Callable接口方式。
/*
实现线程的第三种方式:
实现Callable接口
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
*/
public class ThreadTest15 {
public static void main(String[] args) throws Exception {
// 第一步:创建一个“未来任务类”对象。
// 参数非常重要,需要给一个Callable接口实现类对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
// 线程执行一个任务,执行之后可能会有一个执行结果
// 模拟执行
System.out.println("call method begin");
Thread.sleep(1000 * 3);
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!");
}
}
Object 类中的 wait 和 notify
第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象 都有的方法,因为这两个方式是Object类中自带的。 wait方法和notify方法不是通过线程对象调用, 不是这样的:t.wait(),也不是这样的:t.notify()..不对。 第二:wait()方法作用? Object o = new Object(); o.wait();
表示: 让正在o对象上活动的线程进入等待状态,无期限等待, 直到被唤醒为止。 o.wait();方法的调用,会让“当前线程(正在o对象上 活动的线程)”进入等待状态。
第三:notify()方法作用? Object o = new Object(); o.notify();
表示: 唤醒正在o对象上等待的线程。
还有一个notifyAll()方法: 这个方法是唤醒o对象上处于等待的所有线程。


import java.util.ArrayList;
import java.util.List;
/*
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;
}
@Override
public 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;
}
@Override
public 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();
}
}
}
}
常见报错
- 空指针异常:
NullPointerException - 类型转换异常:
ClassCastException - 数组下标越界异常:
ArrayIndexOutOfBoundsException - 数字格式化异常:
NumberFormatException


