java基础语法

一、java基本语法

1.java中基本数据类型:(从小到大)

byte-short-char-int-long-float-double 还有一个boolean

java中引用数据类型:

数组 类 接口 注解 枚举

java中默认数据类型:

整数类型是int,浮点类型是double

初始值问题:

成员变量:都有初始值
局部变量:基本数据类型无初始值,引用数据类型有初始值
成员变量:都有初始值
局部变量:基本数据类型无初始值,引用数据类型有初始值
​ 整数 0
​ 小数 0.0
​ 布尔 false
​ 字符 ‘\u0000’
​ 引用 null

2.+号在输出语句中各种情况.

如果没有字符串的情况下 + 就代表运算
如果+和字符串合起来使用,+不再有运算的意思了,变成了拼接的意思
任何类型遇到字符串都会变成字符串

3.常用DOS命令

操作说明
盘符名称:盘符切换。E:回车,表示切换到E盘。
dir查看当前路径下的内容。
cd 目录进入单级目录。
cd 目录1\目录2…进入多级目录。cd itheima\JavaSE
cd …回退到上一级目录。
cd \回退到盘符目录。
cls清屏。
exit退出命令提示符窗口。JDK 和 JRE

4.jdk和jre以及jvm

jdk中有jre,jre中有jvm
jdk是开发工具包
jre是程序运行环境

5.注释

单行注释://
多行注释:/**/
文档注释:/*内容/

6.类型转换

大转小需强转,小转大(注意float和long类型)自动转.

注意: i = 0.2; i = i0.2

7.switch语句结构

switch(表达式){
case 常量值1:
语句块1;
【break;】
case 常量值2:
语句块2;
【break;】
。。。
【default:
语句块n+1;
【break;】

}

8.for循环

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

​ 循环体语句③}

(1) 循环条件必须是boolean类型

(2) 第一步:执行初始化语句①,完成循环变量的初始化;

(3) 第二步:执行循环条件语句②,看循环条件语句的值是true,还是false;

(4) 第三步:执行循环体语句③

(5) 第四步:执行迭代语句④,针对循环变量重新赋值

(6) 第五步:根据循环变量的新值,重新从第二步开始再执行一遍

return、break和continue区别

(1)break语句

  1. break语句用于终止某个语句块的执行
  2. break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块
label1: 	{   ……        
	label2:	         {   ……
	label3:			{   ……
				           break label2;
				           ……
					}
			          }
			 } 

(2) continue 语句

  1. continue语句用于跳过某个循环语句块的一次执行
  2. continue语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环

(3)return 语句

  1. return:并非专门用于结束循环的,它的功能是结束一个方法。当一个方法执行到一个return语句时,这个方法将被结束。
  2. 与break和continue不同的是,return直接结束整个方法,不管这个return处于多少层循环之内

break只能用于switch语句和循环语句中。
continue 只能用于循环语句中。
二者功能类似,但continue是终止本次循环,break是终止本层循环。
break、continue之后不能有其他的语句,因为程序永远不会执行其后的语句。
标号语句必须紧接在循环的头部。

9.变量使用时注意事项.

局部变量:第一次不赋值,不报错,但不能使用.
成员变量:第一次不赋值,为默认初始值.
在同一个作用域(一对大括号就是一个作用域)中,不能连续定义多个相同名字的变量

10.标识符

(1).概述:我们自己给类,方法,变量取得名字

(2).硬性规定(必须遵守)

-标识符可以由中文组成,但不推荐使用

- 标识符可以包含英文字母26个(区分大小写)0-9数字$(美元符号)_(下划线)

- 标识符不能以数字开头。int 1a = 100; public class 1Hello{}

- 标识符不能是关键字。int public = 10;

(3).软性建议(建议遵守规则):

给类取名:每一个单词首字母都大写(大驼峰式) DemoTest

给方法取名字: 从第二个单词开始往后(包括第二个单词)首字母大写(小驼峰式) demoTest

给变量取名字: 从第二个单词开始往后(包括第二个单词)首字母大写(小驼峰式) demoTest

取名字的时候最好要:见名知意

11.内存

1、程序计数器(寄存器):当前线程所执行的字节码行号指示器

2、本地方法栈:同虚拟机栈,只不过本地方法栈为虚拟机使用到的native方法服务。

3、虚拟机栈:每个方法在执行的同时都会创建一个栈帧用来存放存储局部变量表、操作栈、动态连接、方法返回地址等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

4、堆:所有线程共享的一块内存区域。Java虚拟机所管理的内存中最大的一块,因为该内存区域的唯一目的就是存放对象实例。几乎所有的对象实例度在这里分配内存,也就是通常我们说的new对象,同时堆也是垃圾收集器管理的主要区域。

5、方法区(元空间):和堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量 、静态变量、和编译器即时编译后的代码等.(也就是存在:常量池, static数据, 每个字节码的Class对象)

注意:java启动一个线程或者开启一个栈消耗64kb.
​ 堆内存满会抛出OOM错误
​ 栈内存满后,会抛出StackOverflowError错误

12.方法里void 和return需要注意事项.

void 和 [return 结果] 不能共存的 (void 代表无返回值 return 结果代表有返回值,将结果返回,之后结束方法).
但是 void 和 return 能共存(return后面不跟任何数据时,它仅仅代表结束方法).

13.方法重载

在同一个类中,方法名相同,参数列表不同的方法.
需参数个数不同
需参数类型不同
需参数类型的顺序不同
和返回值类型无关
和参数名无关

14.记事本执行java文件

1.将 Java 代码编写到扩展名为 .java 的文件中。
2.通过 javac 命令对该 java 文件进行编译。
​ 不带包文件编译: javac 文件名.java
​ 带包文件编译: javac -d . 文件名.java

3.通过 java 命令对生成的 class 文件进行运行。
​ 不带包class文件运行: java 文件名
​ 带包class文件运行: java 包名.文件名

15.注意事项:

1.&& 和 & , || 和 | 的区别?

​ 1).单 & 和 | 在二进制中可能是位运算符.
​ 2).双 && 和 || 称简介与简介或 前面若不成立,不需要看后面.

2.Scanner 中的 next() 和 nextline() 的区别?

​ 1)next()中不能有空格,否则只能返回第一个空格之前的字符串.
​ 2)nextline的前面不能是nextInt().

二、面向对象

1. 面向对象的三大特征

  • 封装
  • 继承
  • 多态
  1. 类是对现实世界事物的描述,是抽象的、概念上的定义
  2. 对象是实际存在的该类事物的一个个体,因而也称实例(instance)

2 对象在内存中存储方式

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.1 匿名对象

在这里插入图片描述
在这里插入图片描述

2.2 构造器

根据参数不同,构造器可以分为如下两类:
隐式无参构造器(编译器默认提供)
显式定义一个或多个构造器(无参、有参)

注意:
1.使用this()必须放在构造器的首行!
2.使用this调用本类中其他的构造器,保证至少有一个构造器是不用this的。

2.3 封装

冒泡排序
在这里插入图片描述
单例模式
在这里插入图片描述
上述懒汉模式的写法存在线程不安全的问题,即当多个线程调用这个单例的时候,可能会出现重复创建实例的现象,可以通过以下方式来解决:
在这里插入图片描述
在这里插入图片描述

2.4 继承

方法的覆盖(override)

  • 定义:在子类中可以根据需要对从父类中继承来的方法进行改造,也称方法的重写、重置。在程序执行时,子类的方法将覆盖父类的方法。
  • 覆盖方法必须和被重写方法具有相同的方法名称、参数列表和返回值类型。
    覆盖方法不能使用比被重写方法更严格的访问权限。
    覆盖和被覆盖的方法必须同时为非static的。
    子类方法抛出的异常不能大于父类被重写方法的异常

关键字super

  • 在Java类中使用super来调用父类中的指定操作:
    super可用于访问父类中定义的属性
    super可用于调用父类中定义的成员方法
    super可用于在子类构造方法中调用父类的构造器
  • 注意:
    尤其当子父类出现同名成员时,可以用super进行区分
    super的追溯不仅限于直接父类
    super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

调用父类的构造器

  • 子类中所有的构造器默认都会调用父类中空参数的构造器
  • 当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器,且必须放在构造器的第一行
  • 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错
    在这里插入图片描述

2.5 多态性

  • 多态性,是面向对象中最重要的概念,在java中有两种体现:
    1 对象的多态性
    2 引用变量的多态性
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    instanceof 操作符
    x instanceof A:检验x是否为类A的对象,返回值为boolean型。
  • 要求x所属的类与类A必须是子类和父类的关系,否则编译错误。
  • 如果x属于类A的子类B,x instanceof A值也为true。
public class Person extends Object {}
public class Student extends Person {}
public class Graduate extends Person {}
-------------------------------------------------------------------
public void method1(Person e) {
	if (e instanceof Person) 
		// 处理Person类及其子类对象
	if (e instanceof Student) 
		//处理Student类及其子类对象
	if (e instanceof Graduate)
		//处理Graduate类及其子类对象

对象类型转换 (Casting )

  • 基本数据类型的Casting:
    自动类型转换:小的数据类型可以自动转换成大的数据类型
    如long g=20; double d=12.0f
    强制类型转换:可以把大的数据类型强制转换(casting)成小的数据类型
    如 float f=(float)12.0; int a=(int)1200L
  • 对Java对象的强制类型转换称为造型
    从子类到父类的类型可以自动进行
    从父类到子类的类型转换必须通过造型(强制类型转换)实现
    无继承关系的引用类型间的转换是非法的
    在造型前可以使用instanceof操作符测试一个对象的类型
    类型转换
    在这里插入图片描述
    toString() 方法
  • toString()方法在Object类中定义,其返回值是String类型,返回类名和它的哈希码值的十六进制形式。
  • 在进行String与其它类型数据的连接操作时,自动调用toString()方法
    Date now=new Date();
    System.out.println(“now=”+now); 相当于
    System.out.println(“now=”+now.toString());
  • 可以根据需要在用户自定义类型中重写toString()方法
    如String 类重写了toString()方法,返回字符串的值。
    s1=“hello”;
    System.out.println(s1);//相当于System.out.println(s1.toString());
  • 基本类型数据转换为String类型时,调用了对应包装类的toString()方法
    int a=10; System.out.println(“a=”+a);

==和equals的区别

  1. == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址

  2. equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。

  3. 具体要看自定义类里有没有重写Object的equals方法来判断。

  4. 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

2.6 关键字static

在Java类中,可用static修饰属性、方法、代码块、内部类、

被修饰后的成员具备以下特点:

  • 随着类的加载而加载
  • 优先于对象存在
  • 修饰的成员,被所有对象所共享
  • 访问权限允许时,可不创建对象,直接被类调用

2.7 关键字final

在Java中声明类、属性和方法时,可使用关键字final来修饰,表示“最终”

  • final标记的类不能被继承。提高安全性,提高程序的可读性。
    String类、System类、StringBuffer类

  • final标记的方法不能被子类重写。
    Object类中的getClass()。

  • final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。
    final标记的成员变量必须在声明的同时或在每个构造方法中或代码块中显式赋值,然后才能使用。
    final double PI=3.14;

2.8 抽象类(abstract class)

  • 用abstract关键字来修饰一个类时,这个类叫做抽象类;
  • 用abstract来修饰一个方法时,该方法叫做抽象方法。
    抽象方法:只有方法的声明,没有方法的实现。以分号结束:abstract int abstractMethod( int a );
  • 含有抽象方法的类必须被声明为抽象类。
  • 抽象类不能被实例化。抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。
  • 不能用abstract修饰属性、私有方法、构造器、静态方法、final的方法。

抽象类与具体类

  • 具体类 — 对现实世界一种实体的抽象定义。

  • 抽象类 — 对现实世界某一类的多种不同实体的统一抽象定义。

  • 模板方法设计模式(TemplateMethod)
    抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
    解决的问题:
    当功能内部一部分实现是确定,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
    编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,就是一种模板模式。

2.9 接口

接口的用途是用来定义现实世界不同类型事物的共同行为特征。
接口可以包含以下成员:

  • 属性
    接口中的所有属性均被视静态常量。例如,下面几种方式的声明是等效的:
    int num = 10;
    public int num = 10;
    public static final int num = 10;
  • 抽象方法
    接口中所有方法均为抽象方法。例如,下面两种方式的声明是等效的:
    public abstract void takeoff();
    public void takeoff();

有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。

  • 一个类可以实现多个接口,
    接口不能被实例化
    具体类(子类)可以实现接口(父类) ,并实现接口中的全部抽象方法
    class SubClass implements InterfaceA{ }
    具体类适用父接口的多态
    接口也可以继承其它接口。

实现接口的类中必须提供接口中所有方法的具体实现内容,方可实例化。否则,仍为抽象类。
接口的主要用途就是被实现类实现。(面向接口编程)

接口小结

  • 用interface来定义。
    接口中的所有成员变量都默认是由public static final修饰的。
    接口中的所有方法都默认是由public abstract修饰的。
    接口没有构造器。
    接口采用多继承机制。

接口用法总结

  • 通过接口可以实现不相关类的相同行为,而不需要考虑这些类之间的层次关系。
    通过接口可以指明多个类需要实现的方法,一般用于定义对象的扩张功能。
    接口主要用来定义规范。解除耦合关系。

接口默认方法的“类优先”原则

  • 若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时
  • 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
  • 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须覆盖该方法来解决冲突。

3.0 内部类

在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。
Inner class一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。
Inner class的名字不能与包含它的类名相同;
Inner class可以使用外部类的私有数据,因为它是外部类的成员,同一个类的成员之间可相互访问。而外部类要访问内部类中的成员需要:内部类.成员或者内部类对象.成员。
分类:成员内部类(static成员内部类嵌套类和成员内部类)
局部内部类(不谈修饰符)、匿名内部类

3.1 匿名内部类

引入
当我们在开发过程中,需要用到一个抽象类的子类的对象或一个接口的实现类的对象,而且只创建一个对象,而且逻辑代码也不复杂。那么我们原先怎么做的呢?
(1)编写类,继承这个父类或实现这个接口
(2)重写父类或父接口的方法
(3)创建这个子类或实现类的对象
例如:

public interface Runnable{
    public abstract void run();
}
//声明接口实现类
public class MyRunnable implements Runnable{
    public void run(){
        while(true){
            System.out.println("大家注意安全");
            try
            	Thread.sleep(1000);
            }catch(Exception e){                
            }
        }
    }
}
public class Test{
    public static void main(String[] args){
        //如果MyRunnable类只是在这里使用一次,并且只创建它的一个对象
        //分开两个.java源文件,反而不好维护
        Runnable target = new MyRunnable();
        Thread t = new Thread("安全提示线程",target);
        t.start();
    }
}

这里,因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。

可以修改为如下形式:

public class Test{
    public static void main(String[] args){
        //MyRunnable类只是在这里使用一次,并且只创建它的一个对象,那么这些写代码更紧凑,更好维护
        Runnable target = new Runnable(){
            public void run(){
                while(true){
                    System.out.println("大家注意安全");
                    try
                        Thread.sleep(1000);
                    }catch(Exception e){                
                    }
                }
            }
        };
        Thread t = new Thread("安全提示线程",target);
        t.start();
    }
}

语法格式

new 父类(【实参列表】){
    重写方法...
}
new 父接口(){
    重写方法...
}

匿名内部类是没有名字的类,因此在声明类的同时就创建好了唯一的对象

思考:这个对象能做什么呢?
答:(1)调用某个方法(2)赋值给父类/父接口的变量,通过多态引用使用这个对象(3)作为某个方法调用的实参
匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。一个匿名内部类一定是在new的后面,用其隐含实现一个接口或继承一个类。

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

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

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

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

使用方式三:匿名内部类的对象作为实参

interface A{
	void method();
}
public class Test{
    public static void test(A a){
    	a.method();
    }
    
    public static void main(String[] args){
    	test(new A(){

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

三、枚举

JDK1.5之前需要自定义枚举类
JDK 1.5 新增的 enum 关键字用于定义枚举类
若枚举只有一个成员, 则可以作为一种单例模式的实现方式
当一个类的对象是可数的情况下,就可以使用枚举.

枚举类的主要方法:

  • values()方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。

  • valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常。

  • 必须在枚举类的第一行声明枚举类对象。

  • 枚举类和普通类的区别:
    使用 enum 定义的枚举类默认继承了 java.lang.Enum 类
    枚举类的构造器只能使用 private 访问控制符
    枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾). 列出的实例系统会自动添加 public static final 修饰
    JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定

示例代码

public class TestEnum {
	public static void main(String[] args) {
		Season spring = Season.SPRING;
		System.out.println(spring);
	}
}
enum Season{
	SPRING("春"),SUMMER("夏"),AUTUMN("秋"),WINTER("冬");
	private String description;
	
	private Season(String description){
		this.description = description;
	}
	
	public String toString(){//需要手动编写,无法使用Generate toString()...
		return description;
	}
}

枚举类的要求和特点:

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

枚举类实现接口
代码示例

interface Windiness{
	void wind();
}
enum Season implements Windiness{
	SPRING,SUMMER(){
		public void wind(){
			System.out.println("刮台风");
		}
	},AUTUMN,WINTER;
	public void wind(){
		System.out.println("刮风");
	}
}

注解

1 系统预定义的三个最基本的注解
1、@Override

​ 用于检测被修饰的方法为有效的重写方法,如果不是,则报编译错误!

​ 只能标记在方法上。

​ 它会被编译器程序读取。

2、@Deprecated

​ 用于表示被标记的数据已经过时,不建议使用。

​ 可以用于修饰 属性、方法、构造、类、包、局部变量、参数。

​ 它会被编译器程序读取。

3、@SuppressWarnings

​ 抑制编译警告。

​ 可以用于修饰类、属性、方法、构造、局部变量、参数

​ 它会被编译器程序读取。

四、异常和包装类

1 异常

1.1 异常概述

异常 :指的是程序在执行过程中,出现的非正常的情况,如果不处理最终会导致JVM的非正常停止。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.
异常也不是指逻辑代码错误而没有得到想要的结果,例如:求a与b的和,你写成了a-b

1.2 异常体系

异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception
Throwable体系:

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
    • 例如:StackOverflowError和OOM(OutOfMemoryError)。
  • Exception:表示异常,其它因编程错误或偶然的外在因素导致的一般性问题,程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。
    • 例如:空指针访问、试图读取不存在的文件、网络连接中断、数组角标越界

1.3 异常分类

我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。

异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?

  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
  • 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会被编译器检测到(不报错)。(如数组索引越界异常,类型转换异常)。程序员应该积极避免其出现的异常,而不是使用try…catch处理,因为这类异常很普遍,若都使用try…catch或throws处理可能会对程序的可读性和运行效率产生影响。

1.4 异常的处理

Java异常处理的五个关键字:**try、catch、finally、throw、throws

1.5 异常throw

Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。异常对象的生成有两种方式:

  • 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对应异常类的实例对象并抛出——自动抛出
  • 由开发人员手动创建:Exception exception = new ClassCastException();——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,但是一旦throw抛出,就会对程序运行产生影响了。

1.6 声明异常throws

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

1.7 异常注意事项

多个异常使用捕获又该如何处理呢?

  1. 多个异常分别处理。
  2. 多个异常一次捕获,多次处理。(推荐)
  3. 多个异常一次捕获一次处理。

一般我们是使用一次捕获多次处理方式,格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型A  e){try中出现A类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型B  e){try中出现B类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

2 常用类

2.1 包装类

Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而当要使用只针对对象设计的API或新特性(例如泛型),那么基本数据类型的数据就需要用包装类来包装。
在这里插入图片描述

2.2 装箱与拆箱

装箱:把基本数据类型转为包装类对象。

转为包装类的对象,是为了使用专门为对象设计的API和特性

拆箱:把包装类对象拆为基本数据类型。

转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。比较、算术等

基本数值---->包装对象

Integer i1 = new Integer(4);//使用构造函数函数
Integer i2 = Integer.valueOf(4);//使用包装类中的valueOf方法

包装对象---->基本数值

Integer i1 = new Integer(4);
int num1 = i1.intValue();

JDK1.5之后,可以自动装箱与拆箱。

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
          //加法运算完成后,再次装箱,把基本数值转成对象。
Integer i = 1;
Double d = 1;//错误的,1是int类型

总结:对象(引用数据类型)能用的运算符有哪些?
(1)instanceof
(2)=:赋值运算符
(3)==和!=:用于比较地址,但是要求左右两边对象的类型一致或者是有父子类继承关系。
(4)对于字符串这一种特殊的对象,支持“+”,表示拼接。

2.3 包装类的一些API

1、基本数据类型和字符串之间的转换
(1)把基本数据类型转为字符串

int a = 10;
//String str = a;//错误的
//方式一:
String str = a + "";
//方式二:
String str = String.valueOf(a);

(2)把字符串转为基本数据类型

String转换成对应的基本类型 ,除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:

  • public static byte parseByte(String s):将字符串参数转换为对应的byte基本类型。
  • public static short parseShort(String s):将字符串参数转换为对应的short基本类型。
  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static float parseFloat(String s):将字符串参数转换为对应的float基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。
  • public static boolean parseBoolean(String s):将字符串参数转换为对应的boolean基本类型。
int a = Integer.parseInt("整数的字符串");
double a = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。
2、数据类型的最大最小值

Integer.MAX_VALUE和Integer.MIN_VALUE
Long.MAX_VALUE和Long.MIN_VALUE
Double.MAX_VALUE和Double.MIN_VALUE

3、转大小写

Character.toUpperCase('x');
Character.toLowerCase('X');

2.4 常用类

java.util.Random
用于产生随机数

- boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 boolean 值。 
- void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。 
- double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.01.0 之间均匀分布的 double 值。 
- float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.01.0 之间均匀分布的 float 值。 
- double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的 double 值,其平均值是 0.0,标准差是 1.0- int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。 
- int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在 0(包括)和指定值(不包括)之间均匀分布的 int 值。 
- long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。 
	@Test
	public void test03(){
		Random r = new Random();
		System.out.println("随机整数:" + r.nextInt());
		System.out.println("随机小数:" + r.nextDouble());
		System.out.println("随机布尔值:" + r.nextBoolean());
	}

2.5 字符串

2.5.1 字符串的特点

1、字符串String类型本身是final声明的,意味着我们不能继承String。
2、字符串的对象也是不可变对象,意味着一旦进行修改,就会产生新对象

我们修改了字符串后,如果想要获得新的内容,必须重新接受。
如果程序中涉及到大量的字符串的修改操作,那么此时的时空消耗比较高。可能需要考虑使用StringBuilder或StringBuffer的可变字符序列。

3、String对象内部是用字符数组进行保存的

JDK1.9之前有一个char[] value数组,JDK1.9之后byte[]数组

"abc"等效于char[] data={ ‘a’ , ‘b’ , ‘c’ }`。

例如: 
String str = "abc";
相当于: 
char data[] = {'a', 'b', 'c'};     
String str = new String(data);
// String底层是靠字符数组实现的。

4、String类中这个char[] values数组也是final修饰的,意味着这个数组不可变,然后它是private修饰,外部不能直接操作它,String类型提供的所有的方法都是用新对象来表示修改后内容的,所以保证了String对象的不可变。

5、就因为字符串对象设计为不可变,那么所以字符串有常量池来保存很多常量对象
常量池在方法区。
如果细致的划分:
(1)JDK1.6及其之前:方法区
(2)JDK1.7:堆
(3)JDK1.8:元空间

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
// 内存中只有一个"abc"对象被创建,同时被s1和s2共享。
2.5.2 构造字符串对象

1、使用构造方法

- public String() ` :初始化新创建的 String对象,以使其表示空字符序列。
- ` String(String original)`: 初始化一个新创建的 `String` 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
- `public String(char[] value) ` :通过当前参数中的字符数组来构造新的String- `public String(char[] value,int offset, int count) ` :通过字符数组的一部分来构造新的String- `public String(byte[] bytes) ` :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String- `public String(byte[] bytes,String charsetName) ` :通过使用指定的字符集解码当前参数中的字节数组来构造新的String

构造举例,代码如下:

//字符串常量对象
String str = "hello";

// 无参构造
String str1 = new String();

//创建"hello"字符串的副本
String str2 = new String("hello");

//通过字符数组构造
char chars[] = {'a', 'b', 'c','d','e'};     
String str3 = new String(chars);
String str4 = new String(chars,0,3);

// 通过字节数组构造
byte bytes[] = { 97, 98, 99 };     
String str5 = new String(bytes);
String str6 = new String(bytes,"GBK");

2、使用静态方法

- static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String
- static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String
- static String valueOf(char[] data)  : 返回指定数组中表示该字符序列的 String
- static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String
- static String valueOf(xx  value):xx支持各种数据类型,返回各种数据类型的value参数的字符串表示形式。

3、使用""+
任意数据类型与"字符串"进行拼接,结果都是字符串

	public static void main(String[] args) {
		int num = 123456;
		String s = num + "";
		System.out.println(s);
		
		Student stu = new Student();
		String s2 = stu + "";//自动调用对象的toString(),然后与""进行拼接
		System.out.println(s2);
	}
2.5.3 可变字符序列

因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低。因此,JDK又在java.lang包提供了可变字符序列StringBuilder和StringBuffer类型。

  • StringBuffer:老的,线程安全的(因为它的方法有synchronized修饰)

  • StringBuilder:线程不安全的

常用的API,StringBuilder、StringBuffer的API是完全一致的
(1)append(xx):拼接,追加
(2)insert(int index, xx):插入
(3)delete(int start, int end), deleteCharAt(int index)
(4)set(int index, xx)
(5)reverse():反转
… 替换、截取、查找…

@Test
	public void test6(){
		StringBuilder s = new StringBuilder("helloworld");
		s.setLength(30);
		System.out.println(s);
	}
	@Test
	public void test5(){
		StringBuilder s = new StringBuilder("helloworld");
		s.setCharAt(2, 'a');
		System.out.println(s);
	}
	
	
	@Test
	public void test4(){
		StringBuilder s = new StringBuilder("helloworld");
		s.reverse();
		System.out.println(s);
	}
	
	@Test
	public void test3(){
		StringBuilder s = new StringBuilder("helloworld");
		s.delete(1, 3);
		s.deleteCharAt(4);
		System.out.println(s);
	}
	
	
	@Test
	public void test2(){
		StringBuilder s = new StringBuilder("helloworld");
		s.insert(5, "java");
		s.insert(5, "chailinyan");
		System.out.println(s);
	}
	
	@Test
	public void test1(){
		StringBuilder s = new StringBuilder();
		s.append("hello").append(true).append('a').append(12).append("atguigu");
		System.out.println(s);
		System.out.println(s.length());
	}
2.5.4 日期时间API

java.time及其子包中。

1、LocalDate、LocalTime、LocalDateTime
(1)now():获取系统日期或时间
(2)of(xxx):或者指定的日期或时间
(3)运算:运算后得到新对象,需要重新接受
plusXxx():在当前日期或时间对象上加xx
minusXxx() :在当前日期或时间对象上减xx

 方法                                                          **描述**                                                     
 now() / now(ZoneId zone)                                      静态方法,根据当前时间创建对象/指定时区的对象                
 of()                                                          静态方法,根据指定日期/时间创建对象                          
 getDayOfMonth()/getDayOfYear()                                获得月份天数(1-31) /获得年份天数(1-366)                      
 getDayOfWeek()                                                获得星期几(返回一个 DayOfWeek 枚举值)                        
 getMonth()                                                   获得月份, 返回一个 Month 枚举值                              
 getMonthValue() / getYear()                                   获得月份(1-12) /获得年份                                     
 getHours()/getMinute()/getSecond()                            获得当前对象对应的小时、分钟、秒                             
 withDayOfMonth()/withDayOfYear()/withMonth()/withYear()      | 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象 |
 with(TemporalAdjuster  t)                                     将当前日期时间设置为校对器指定的日期时间                     
 plusDays(), plusWeeks(), plusMonths(), plusYears(),plusHours() | 向当前对象添加几天、几周、几个月、几年、几小时               |
 minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours() | 从当前对象减去几月、几周、几天、几年、几小时                 |
 plus(TemporalAmount t)/minus(TemporalAmount t)                添加或减少一个 DurationPeriod                            
 isBefore()/isAfter()                                          比较两个 LocalDate                                           
 isLeapYear()                                                  判断是否是闰年(在LocalDate类中声明)                        
 format(DateTimeFormatter  t)                                  格式化本地日期、时间,返回一个字符串                         
 parse(Charsequence text)                                      将指定格式的字符串解析为日期、时间                           

2、DateTimeFormatter:日期时间格式化

该类提供了三种格式化方法:
预定义的标准格式。如:ISO_DATE_TIME;ISO_DATE
本地化相关的格式。如:ofLocalizedDate(FormatStyle.MEDIUM)
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

	@Test
	public void test10(){
		LocalDateTime now = LocalDateTime.now();
		
//		DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);//2019年6月6日 下午04时40分03秒
		DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);//19-6-6 下午4:40
		String str = df.format(now);
		System.out.println(str);
	}
	@Test
	public void test9(){
		LocalDateTime now = LocalDateTime.now();
		
		DateTimeFormatter df = DateTimeFormatter.ISO_DATE_TIME;//2019-06-06T16:38:23.756
		String str = df.format(now);
		System.out.println(str);
	}
	
	@Test
	public void test8(){
		LocalDateTime now = LocalDateTime.now();
		
		DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒  SSS毫秒  E 是这一年的D天");
		String str = df.format(now);
		System.out.println(str);
	}
	
	@Test
	public void test7(){
		LocalDate now = LocalDate.now();
		LocalDate before = now.minusDays(100);
		System.out.println(before);//2019-02-26
	}
	
	@Test
	public void test06(){
		LocalDate lai = LocalDate.of(2019, 5, 13);
		LocalDate go = lai.plusDays(160);
		System.out.println(go);//2019-10-20
	}
	
	@Test
	public void test05(){
		LocalDate lai = LocalDate.of(2019, 5, 13);
		System.out.println(lai.getDayOfYear());
	}
	
	
	@Test
	public void test04(){
		LocalDate lai = LocalDate.of(2019, 5, 13);
		System.out.println(lai);
	}
	
	@Test
	public void test03(){
		LocalDateTime now = LocalDateTime.now();
		System.out.println(now);
	}
	
	@Test
	public void test02(){
		LocalTime now = LocalTime.now();
		System.out.println(now);
	}
	
	@Test
	public void test01(){
		LocalDate now = LocalDate.now();
		System.out.println(now);
	}

五 多线程

1 创建线程

1.1 继承Thread类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程
public class Demo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}
public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}

1.2 实现Runnable接口

Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。
    代码如下:
  public class MyRunnable implements Runnable{
  	@Override  
      public void run() {
          for (int i = 0; i < 20; i++) {
          	System.out.println(Thread.currentThread().getName()+" "+i);         
  		}       
  	}    
  }
  public class Demo {
      public static void main(String[] args) {
          //创建自定义类对象  线程任务对象
          MyRunnable mr = new MyRunnable();
          //创建线程对象
          Thread t = new Thread(mr, "小强");
          t.start();
          for (int i = 0; i < 20; i++) {
              System.out.println("旺财 " + i);
          }
      }
  }

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现
Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。
而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法

1.3 使用匿名内部类对象来实现线程的创建和启动

new Thread("新的线程!"){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}.start();
new Thread(new Runnable(){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+":" + i);
		}
	}
}).start();

2 Thread类

2.1 构造方法

public Thread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2.2 常用方法系列1

 public void run() :此线程要执行的任务在此处定义代码。
 public String getName() :获取当前线程名称。
 public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
 public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。 
 public final int getPriority() :返回线程优先级 
 public final void setPriority(int newPriority) :改变线程的优先级
 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
 MAX_PRIORITY(10):最高优先级 
 MIN _PRIORITY (1):最低优先级
 NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
    public static void main(String[] args) {
       Thread t = new Thread(){
           @Override
           public void run() {
               System.out.println(getName()+"的优先级"+getPriority());
           }
       };
       t.setPriority(Thread.MAX_PRIORITY);
       t.start();

        System.out.println(Thread.currentThread().getName()+"优先级"+Thread.currentThread().getPriority());
    }

2.3 常用方法系列2

 public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
 public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
 public static void yield()yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
 void join() :等待该线程终止。 
 void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。 
 void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 
 public final void stop():强迫线程停止执行。 该方法具有固有的不安全性,已经标记为@Deprecated不建议再使用,那么我们就需要通过其他方式来停止线程了,其中一种方式是使用变量的值的变化来控制线程是否结束。

示例代码:倒计时

	public static void main(String[] args) {
		for (int i = 10; i>=0; i--) {
			System.out.println(i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("新年快乐!");
	}

2.4 线程安全

2.4.1 线程安全

我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个
(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)
方式一:

public class Ticket implements Runnable {
	private int ticket = 100;

	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		// 每个窗口卖票的操作
		// 窗口永远开启
		while (true) {
            if (ticket > 0) { // 有票可以卖
                    // 出票操作
                // 使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticket--);
            }
		}
	}
}

测试类:

public class Demo {
	public static void main(String[] args) {
		// 创建线程任务对象
		Ticket ticket = new Ticket();
		// 创建三个窗口对象
		Thread t1 = new Thread(ticket, "窗口1");
		Thread t2 = new Thread(ticket, "窗口2");
		Thread t3 = new Thread(ticket, "窗口3");
		// 同时卖票
		t1.start();
		t2.start();
		t3.start();
	}
}

方式二:

public class TestThread {
	public static void main(String[] args) {
		Ticket t1 = new Ticket("窗口一");
		Ticket t2 = new Ticket("窗口二");
		Ticket t3 = new Ticket("窗口三");
		
		t1.start();
		t2.start();
		t3.start();
	}
}

class Ticket extends Thread{
	private static int ticket = 100;

	public Ticket() {
		super();
	}

	public Ticket(String name) {
		super(name);
	}

	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		// 每个窗口卖票的操作
		// 窗口永远开启
		while (true) {
            if (ticket > 0) { // 有票可以卖
                // 出票操作
                // 使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取当前线程对象的名字
                System.out.println(getName() + "正在卖:" + ticket--);
            }
		}
	}
}

发现程序出现了两个问题:

  1. 相同的票数,比如某张票被卖了两回。
  2. 不存在的票,比如0票与-1票,是不存在的。
    这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制
(synchronized)来解决。
在这里插入图片描述

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制

2.4.2 同步代码块

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

synchronized(同步锁){
     需要同步操作的代码
}
  • 同步锁:
    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

​ 锁对象 可以是任意类型。

​ 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:

public class Ticket implements Runnable {
	private int ticket = 100;

	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		// 每个窗口卖票的操作
		// 窗口永远开启
        while (true) {
            synchronized (this) {//这里可以选择this作为锁,是因为对于这几个线程,Ticket的this是同一个 
                if (ticket > 0) {// 有票可以卖
                    // 出票操作
                    // 使用sleep模拟一下出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 获取当前线程对象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在卖:" + ticket--);
                }
			}
        }
	}
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

class Ticket extends Thread{
	private static int ticket = 100;

	public Ticket() {
		super();
	}

	public Ticket(String name) {
		super(name);
	}

	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		// 每个窗口卖票的操作
		// 窗口永远开启
		while (true) {
			synchronized (Ticket.class) {//这里不能选用this作为锁,因为这几个线程的this不是同一个
				if (ticket > 0) { // 有票可以卖
	                // 出票操作
	                // 使用sleep模拟一下出票时间
	                try {
	                    Thread.sleep(100);
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	                // 获取当前线程对象的名字
	                System.out.println(getName() + "正在卖:" + ticket--);
	            }
			}
		}
	}
}

2.4.3 同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外
    等着

格式:

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

同步方法的锁对象:
(1)静态方法:当前类的Class对象
(2)非静态方法:this
使用同步方法代码如下:

public class Ticket implements Runnable{
	private int ticket = 100;   
	/*
	 * 执行卖票操作   
	 */  
	@Override 
	public void run() {	    
	//每个窗口卖票的操作 	        
	//窗口 永远开启 	        
		while(true){        
			sellTicket();            
		}        
	}
		        
		/*   
		 * 锁对象 是 谁调用这个方法 就是谁     
		 *   隐含 锁对象 就是  this    
		 *    	    
		 */
	    
	public synchronized void sellTicket(){
		if(ticket>0){//有票 可以卖   
		  //出票操作
		  //使用sleep模拟一下出票时间 
			try {              
				Thread.sleep(100);
			} catch (InterruptedException e) {
  				e.printStackTrace();
			}
        
            //获取当前线程对象的名字 
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
   }
}
class Ticket extends Thread {
	private static int ticket = 100;

	public Ticket() {
		super();
	}

	public Ticket(String name) {
		super(name);
	}

	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		// 每个窗口卖票的操作
		// 窗口永远开启
		while (true) {
			sellTicket();
		}
	}
	//这里必须是静态方法,因为如果是非静态方法,隐含的锁对象是this,那么多个线程就不是同一个锁对象了
	//而静态方法隐含的锁对象是当前类的Class对象
	public synchronized static void sellTicket(){
		if(ticket>0){//有票可以卖 
			//出票操作
			//使用sleep模拟一下出票时间
			try {
				Thread.sleep(100);
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
			//获取当前线程对象的名字
			String name = Thread.currentThread().getName();
			System.out.println(name + "正在卖:" + ticket--);
		}
	}

}

2.5 等待唤醒机制

什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

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

注意:

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
  • 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

生产者与消费者问题
等待唤醒机制可以解决经典的“生产者与消费者”的问题。

生产者与消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

生产者与消费者问题中其实隐含了两个问题:

  • 线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。
  • 线程的协调工作问题:
    • 要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题。

一对一做包子吃包子问题
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

代码演示:
包子资源类:

public class BaoZi {
     String  pier ;
     String  xianer ;
     boolean  flag = false ;//包子资源 是否准备好  包子资源状态
}
public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//没包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }                
                
                System.out.println("吃货正在吃:"+bz.pier+","+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}
public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子资源  存在
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 没有包子  造包子
                System.out.println("包子铺开始做包子");
                if(count%2 == 0){
                    // 薄皮  蟹黄包
                    bz.pier = "薄皮";
                    bz.xianer = "蟹黄灌汤";
                }else{
                    // 厚皮  牛肉大葱
                    bz.pier = "厚皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程 (吃货)
                bz.notify();
            }
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        //等待唤醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃货",bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz);

        ch.start();
        bzp.start();
    }
}

2.6 线程生命周期

简单来说,线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。
在这里插入图片描述

2.7 Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
  5. 扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用
    java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

2.8 释放锁操作与死锁

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?

1、释放锁的操作

当前线程的同步方法、同步代码块执行结束。

当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。

当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。

当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

2、不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。

线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()这样的过时来控制线程。

3、死锁

不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

public class TestDeadLock {
	public static void main(String[] args) {
		Object g = new Object();
		Object m = new Object();
		Owner s = new Owner(g,m);
		Customer c = new Customer(g,m);
		new Thread(s).start();
		new Thread(c).start();
	}
}
class Owner implements Runnable{
	private Object goods;
	private Object money;

	public Owner(Object goods, Object money) {
		super();
		this.goods = goods;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (goods) {
			System.out.println("先给钱");
			synchronized (money) {
				System.out.println("发货");
			}
		}
	}
}
class Customer implements Runnable{
	private Object goods;
	private Object money;

	public Customer(Object goods, Object money) {
		super();
		this.goods = goods;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (money) {
			System.out.println("先发货");
			synchronized (goods) {
				System.out.println("再给钱");
			}
		}
	}
}

4、sleep()和wait()方法的区别

(1)sleep()不释放锁,wait()释放锁

(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll

(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明

因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。

六、泛型

1 泛型的概念

1.1 泛型的引入

DK1.5设计了泛型的概念。泛型即为“类型参数”,这个类型参数在声明它的类、接口或方法中,代表未知的通用的类型。例如:

java.lang.Comparable接口和java.util.Comparator接口,是用于对象比较大小的规范接口,这两个接口只是限定了当一个对象大于另一个对象时返回正整数,小于返回负整数,等于返回0。但是并不确定是什么类型的对象比较大小,之前的时候只能用Object类型表示,使用时既麻烦又不安全,因此JDK1.5就给它们增加了泛型。

public interface Comparable<T>{
    int compareTo(T o) ;
}
public interface Comparator<T>{
     int compare(T o1, T o2) ;
}

其中就是类型参数,即泛型。

1.2 参数类型:泛型类与泛型接口

当我们在声明类或接口时,类或接口中定义某个成员时,该成员有些类型是不确定的,而这个类型在使用这个类或接口时可以确定,那么我们可以使用泛型。
语法格式:

【修饰符】 class 类名<类型变量列表>{
    
}
【修饰符】 interface 接口名<类型变量列表>{
    
}

注意:

  • <类型变量列表>:可以是一个或多个类型变量,一般都是使用单个的大写字母表示。例如:、<K,V>等。
  • <类型变量列表>中的类型变量不能用于静态成员上。

示例代码:
例如:我们要声明一个学生类,该学生包含姓名、成绩,而此时学生的成绩类型不确定,为什么呢,因为,语文老师希望成绩是“优秀”、“良好”、“及格”、“不及格”,数学老师希望成绩是89.5, 65.0,英语老师希望成绩是’A’,‘B’,‘C’,‘D’,‘E’。那么我们在设计这个学生类时,就可以使用泛型。

public class Student<T>{
	private String name;
	private T score;
	
	public Student() {
		super();
	}
	public Student(String name, T score) {
		super();
		this.name = name;
		this.score = score;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public T getScore() {
		return score;
	}
	public void setScore(T score) {
		this.score = score;
	}
	@Override
	public String toString() {
		return "姓名:" + name + ", 成绩:" + score;
	}
}

1.3 使用泛型类与泛型接口

在使用这种参数化的类与接口时,我们需要指定泛型变量的实际类型参数:
(1)实际类型参数必须是引用数据类型,不能是基本数据类型
(2)在创建类的对象时指定类型变量对应的实际类型参数

public class TestGeneric{
	public static void main(String[] args) {
		//语文老师使用时:
		Student<String> stu1 = new Student<String>("张三", "良好");
        
		//数学老师使用时:
        //Student<double> stu2 = new Student<double>("张三", 90.5);//错误,必须是引用数据类型
		Student<Double> stu2 = new Student<Double>("张三", 90.5);
        
		//英语老师使用时:
		Student<Character> stu3 = new Student<Character>("张三", 'C');
        
        //错误的指定
        //Student<Object> stu = new Student<String>();//错误的
	}
}

JDK1.7支持简写形式:Student stu1 = new Student<>(“张三”, “良好”);
指定泛型实参时,必须左右两边一致,不存在多态现象

(3)在继承参数化的类或实现参数接口时,指定类型变量对应的实际类型参数

class ChineseStudent extends Student<String>{

	public ChineseStudent() {
		super();
	}

	public ChineseStudent(String name, String score) {
		super(name, score);
	}
	
}
public class TestGeneric{
	public static void main(String[] args) {
		//语文老师使用时:
		ChineseStudent stu = new ChineseStudent("张三", "良好");
	}
}

1.4 类型变量的上限

当在声明类型变量时,如果不希望这个类型变量代表任意引用数据类型,而是某个系列的引用数据类型,那么可以设定类型变量的上限。
语法格式:

<类型变量  extends 上限>

如果有多个上限

<类型变量  extends 上限1 & 上限2>

如果多个上限中有类有接口,那么只能有一个类,而且必须写在最左边。接口的话,可以多个。
如果在声明<类型变量>时没有指定上限,默认上限是java.lang.Object。

例如:我们要声明一个两个数求和的工具类,要求两个加数必须是Number数字类型,并且实现Comparable接口。

class SumTools<T extends Number & Comparable<T>>{
	private T a;
	private T b;
	public SumTools(T a, T b) {
		super();
		this.a = a;
		this.b = b;
	}
	@SuppressWarnings("unchecked")
	public T getSum(){
		if(a instanceof BigInteger){
			return (T) ((BigInteger) a).add((BigInteger)b);
		}else if(a instanceof BigDecimal){
			return (T) ((BigDecimal) a).add((BigDecimal)b);
		}else if(a instanceof Short){
			return (T)(Integer.valueOf((Short)a+(Short)b));
		}else if(a instanceof Integer){
			return (T)(Integer.valueOf((Integer)a+(Integer)b));
		}else if(a instanceof Long){
			return (T)(Long.valueOf((Long)a+(Long)b));
		}else if(a instanceof Float){
			return (T)(Float.valueOf((Float)a+(Float)b));
		}else if(a instanceof Double){
			return (T)(Double.valueOf((Double)a+(Double)b));
		}
		throw new UnsupportedOperationException("不支持该操作");
	}
}

测试类

	public static void main(String[] args) {
		SumTools<Integer> s = new SumTools<Integer>(1,2);
		Integer sum = s.getSum();
		System.out.println(sum);
		
//		SumTools<String> s = new SumTools<String>("1","2");//错误,因为String类型不是extends Number
	}

1.5 泛型擦除

当使用参数化类型的类或接口时,如果没有指定泛型,那么会怎么样呢?
会发生泛型擦除,自动按照最左边的第一个上限处理。如果没有指定上限,上限即为Object。

	public static void main(String[] args) {
		SumTools s = new SumTools(1,2);
		Number sum = s.getSum();
		System.out.println(sum);
	}
import java.util.Comparator;

public class CircleComparator implements Comparator{

	@Override
	public int compare(Object o1, Object o2) {
		//强制类型转换
		Circle c1 = (Circle) o1;
		Circle c2 = (Circle) o2;
		return Double.compare(c1.getRadius(), c2.getRadius());
	}
	
}

1.6 类型通配符

当我们声明一个方法时,某个形参的类型是一个参数化的泛型类或泛型接口类型,但是在声明方法时,又不确定该泛型实际类型,我们可以考虑使用类型通配符。
例如:

这个学生类是一个参数化的泛型类,代码如下

public class Student<T>{
	private String name;
	private T score;
	
	public Student() {
		super();
	}
	public Student(String name, T score) {
		super();
		this.name = name;
		this.score = score;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public T getScore() {
		return score;
	}
	public void setScore(T score) {
		this.score = score;
	}
	@Override
	public String toString() {
		return "姓名:" + name + ", 成绩:" + score;
	}
}

<?>任意类型
例如:我们要声明一个学生管理类,这个管理类要包含一个方法,可以遍历学生数组。

学生管理类:

class StudentService {
	public static void print(Student<?>[] arr) {
		for (int i = 0; i < arr.length; i++) {
			System.out.println(arr[i]);
		}
	}
}

测试类

public class TestGeneric {
	public static void main(String[] args) {
		// 语文老师使用时:
		Student<String> stu1 = new Student<String>("张三", "良好");

		// 数学老师使用时:
		// Student<double> stu2 = new Student<double>("张三", 90.5);//错误,必须是引用数据类型
		Student<Double> stu2 = new Student<Double>("张三", 90.5);

		// 英语老师使用时:
		Student<Character> stu3 = new Student<Character>("张三", 'C');

		Student<?>[] arr = new Student[3];
		arr[0] = stu1;
		arr[1] = stu2;
		arr[2] = stu3;

		StudentService.print(arr);
	}
}

<? extends 上限>

例如:我们要声明一个学生管理类,这个管理类要包含一个方法,找出学生数组中成绩最高的学生对象。
要求学生的成绩的类型必须可比较大小,实现Comparable接口。
学生管理类:

class StudentService {
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static Student<? extends Comparable> max(Student<? extends Comparable>[] arr){
		Student<? extends Comparable> max = arr[0];
		for (int i = 0; i < arr.length; i++) {
			if(arr[i].getScore().compareTo(max.getScore())>0){
				max = arr[i];
			}
		}
		return max;
	}
}

测试类

public class TestGeneric {
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static void main(String[] args) {
		Student<? extends Double>[] arr = new Student[3];
		arr[0] = new Student<Double>("张三", 90.5);
		arr[1] = new Student<Double>("李四", 80.5);
		arr[2] = new Student<Double>("王五", 94.5);
		
		Student<? extends Comparable> max = StudentService.max(arr);
		System.out.println(max);
	}
}

<? super 下限>

现在要声明一个数组工具类,包含
(1)方法1:可以给任意对象数组进行从小到大排序,只要你指定定制比较器对象,而且这个定制比较器对象可以是当前数组元素类型自己或其父类的定制比较器对象
(2)方法2:可以将任意对象数组的元素拼接为一个字符串返回
数组工具类:

class MyArrays{
	public static <T> String toString(T[] arr){
		String str = "";
		for (int i = 0; i < arr.length; i++) {
			if(i==0){
				str += "[" + arr[i] + ",";
			}else if(i==arr.length-1){
				str += arr[i] + "]";
			}else{
				str += arr[i] + ",";
			}
		}
		return str;
	}
	
	public static <T> void sort(T[] arr, Comparator<? super T> c){
		for (int i = 1; i < arr.length; i++) {
			for (int j = 0; j < arr.length-i; j++) {
				if(c.compare(arr[j], arr[j+1])>0){
					T temp = arr[j];
					arr[j] = arr[j+1];
					arr[j+1] = temp;
				}
			}
		}
	}
}

例如:有如下JavaBean

class Person{
	private String name;
	private int age;
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public Person() {
		super();
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "name=" + name + ", age=" + age;
	}
}
class Student extends Person{
	private int score;

	public Student(String name, int age, int score) {
		super(name, age);
		this.score = score;
	}

	public Student() {
		super();
	}

	public int getScore() {
		return score;
	}

	public void setScore(int score) {
		this.score = score;
	}

	@Override
	public String toString() {
		return super.toString() + ",score=" + score;
	}
	
}

测试类

public class TestGeneric {
	public static void main(String[] args) {
		Student[] all = new Student[3];
		all[0] = new Student("张三", 23, 89);
		all[1] = new Student("李四", 22, 99);
		all[2] = new Student("王五", 25, 67);
		
		MyArrays.sort(all, new Comparator<Person>() {

			@Override
			public int compare(Person o1, Person o2) {
				return o1.getAge() - o2.getAge();
			}
		});
		
		System.out.println(MyArrays.toString(all));
		
		MyArrays.sort(all, new Comparator<Student>() {

			@Override
			public int compare(Student o1, Student o2) {
				return o1.getScore() - o2.getScore();
			}
		});
		System.out.println(MyArrays.toString(all));
	}
}

七、集合

1 集合框架

  • 集合:集合是java中提供的一种容器,可以用来存储多个数据。

集合和数组既然都是容器,它们有啥区别呢?

  • 数组的长度是固定的。集合的长度是可变的。
  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

为了可以满足用户数据更多种的逻辑关系,而设计的一系列的不同于数组的可变的聚合的抽象数据类型。这些接口和类在java.util包中,因为类型很丰富,因此我们通常称为集合框架集。

集合主要分为两大系列:Collection和Map,Collection 表示一组对象,Map表示一组映射关系或键值对。
在这里插入图片描述
Collection : 可以保存一个一个的对象, 无序可重复.是一个接口

  • 无序 : 不按照添加顺序保存对象. 不用管它的顺序
  • 可重复 : 重复的对象可以都保存进去.
  • boolean add(Object obj) 添加一个元素到集合中, 如果成功返回true, 否则返回false
  • boolean contains(Object ojb) 判断当前集合中是否包含参数中的对象.
  • boolean remove(Object obj) 从当前集合中删除指定的对象
  • int size() 返回当前集合中的对象个数

Set : 无序不可重复(存储无顺所以无下标概念)

  • HashSet : 使用哈希算法实现的Set集合.
  • 认定对象重复 : 两个对象的equals为true, 还得2个对象的hashCode一样.
  • LinkedHashSet : 可以实现有序不可重复
  • TreeSet : 基于二叉树实现的Set集合. : 内部要实现元素的自然排序
  • 认定对象重复 : 两个对象的compare为0
  • 也可以在创建TreeSet对象时, 给它关联一个比较器对象, 让它实现定制排序.
    并且如果对象也能自己比, 会忽略它.
    ** List : 有序可重复(有下标概念), 接照添加顺序依次保存元素, 重复的元素也可以放入**
  • void add(int index, Object obj) 在指定下标位置处插入新元素, 永不失败!!
  • Object get(int index) 获取指定下标处的对象
  • Object remove(int index) 删除指定下标处的对象
  • Object set(int index, Object obj) 替换指定下标处的元素为新对象, 返回老对象
  • ArrayList : 基于数组实现的List集合, 是新的, 线程不安全.
  • Vector : 和ArrayList一样, 是古老的, 线程安全
  • LinkedList : 基于双向链表实现的List集合

注意:各种集合优缺点
Set : 无序不可重复

  • HashSet : 使用哈希算法实现的Set集合
    基于数组, 把对象的哈希码转换为下标 保存对象
    几乎没有缺点.
  • TreeSet : 基于二叉搜索树(红黑树)实现的Set集合 适用于频繁检索
    优点 : 对内存要求低, 检索速度巨快, 因为是二分法查找
    缺点 : 插入和删除数据慢, 因为有大量的比较.

List : 有序可重复

  • ArrayList : 基于数组实现的List集合, 适用于存档数据
    优点 : 末端数据插入删除最快, 检索速度快
    缺点 : 要求内存连续, 非末端操作数据最慢(大量元素的移动)
  • LinkedList : 基于链表实现的List集合 适用于频繁的修改数据, 偶尔检索
    优点 : 对内存要求低, 插入和删除元素非常快
    缺点 : 检索最慢.

2 Collection 常用功能

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
1、添加元素

(1)add(E obj):添加元素对象到当前集合中
(2)addAll(Collection<? extends E> other):添加other集合中的所有元素对象到当前集合中,即this = this ∪ other

2、删除元素

(1) boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true的元素。
(2)boolean removeAll(Collection<?> coll):从当前集合中删除所有与coll集合中相同的元素。即this = this - this ∩ coll

3、判断元素

(1)boolean isEmpty():判断当前集合是否为空集合。
(2)boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素。
(3)boolean containsAll(Collection<?> c):判断c集合中的元素是否在当前集合中都存在。即c集合是否是当前集合的“子集”。

4、查询

(1)int size():获取当前集合中实际存储的元素个数
(2)Object[] toArray():返回包含当前集合中所有元素的数组

5、交集

(1)boolean retainAll(Collection<?> coll):当前集合仅保留与c集合中的元素相同的元素,即当前集合中仅保留两个集合的交集,即this = this ∩ coll;

方法演示:

import java.util.ArrayList;
import java.util.Collection;

public class Demo1Collection {
    public static void main(String[] args) {
		// 创建集合对象 
    	// 使用多态形式
    	Collection<String> coll = new ArrayList<String>();
    	// 使用方法
    	// 添加功能  boolean  add(String s)
    	coll.add("小李广");
    	coll.add("扫地僧");
    	coll.add("石破天");
    	System.out.println(coll);

    	// boolean contains(E e) 判断o是否在集合中存在
    	System.out.println("判断  扫地僧 是否在集合中"+coll.contains("扫地僧"));

    	//boolean remove(E e) 删除在集合中的o元素
    	System.out.println("删除石破天:"+coll.remove("石破天"));
    	System.out.println("操作之后集合中元素:"+coll);
    	
    	// size() 集合中有几个元素
		System.out.println("集合中有"+coll.size()+"个元素");

		// Object[] toArray()转换成一个Object数组
    	Object[] objects = coll.toArray();
    	// 遍历数组
    	for (int i = 0; i < objects.length; i++) {
			System.out.println(objects[i]);
		}

		// void  clear() 清空集合
		coll.clear();
		System.out.println("集合中内容为:"+coll);
		// boolean  isEmpty()  判断是否为空
		System.out.println(coll.isEmpty());  	
	}
}
	@Test
	public void test2(){
		Collection coll = new ArrayList();
		coll.add(1);
		coll.add(2);
		
		System.out.println("coll集合元素的个数:" + coll.size());
		
		Collection other = new ArrayList();
		other.add(1);
		other.add(2);
		other.add(3);
		
		coll.addAll(other);
//		coll.add(other);
		System.out.println("coll集合元素的个数:" + coll.size());
	}

注意:coll.addAll(other);与coll.add(other);
在这里插入图片描述

	@Test
	public void test5(){
		Collection coll = new ArrayList();
		coll.add(1);
		coll.add(2);
		coll.add(3);
		coll.add(4);
		coll.add(5);
		System.out.println("coll集合元素的个数:" + coll.size());//5
		
		Collection other = new ArrayList();
		other.add(1);
		other.add(2);
		other.add(8);
		
		coll.retainAll(other);//保留交集
		System.out.println("coll集合元素的个数:" + coll.size());//2
	}

3 Iterator迭代器

3.1 Iterator接口

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。

想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,下面介绍一下获取迭代器的方法:

  • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。

下面介绍一下迭代的概念:

  • 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
    Iterator接口的常用方法如下:

  • public E next():返回迭代的下一个元素。

  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。

接下来我们通过案例学习如何使用Iterator迭代集合中元素:

public class IteratorDemo {
  	public static void main(String[] args) {
        // 使用多态方式 创建对象
        Collection<String> coll = new ArrayList<String>();

        // 添加元素到集合
        coll.add("串串星人");
        coll.add("吐槽星人");
        coll.add("汪星人");
        //遍历
        //使用迭代器 遍历   每个集合对象都有自己的迭代器
        Iterator<String> it = coll.iterator();
        //  泛型指的是 迭代出 元素的数据类型
        while(it.hasNext()){ //判断是否有迭代元素
            String s = it.next();//获取迭代出的元素
            System.out.println(s);
        }
  	}
}

tips::在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException没有集合元素的错误。

3.2 使用Iterator迭代器删除元素

java.util.Iterator迭代器中有一个方法:

​ void remove() ;

那么,既然Collection已经有remove(xx)方法了,为什么Iterator迭代器还要提供删除方法呢?

因为Collection的remove方法,无法根据条件删除。

例如:要删除以下集合元素中,名字是三个字的人名

	@Test
	public void test02(){
		Collection<String> coll = new ArrayList<>();
		coll.add("陈琦");
		coll.add("李晨");
		coll.add("邓超");
		coll.add("黄晓明");
		
		//删除名字有三个字的
//		coll.remove(o)//无法编写
		
		Iterator<String> iterator = coll.iterator();
		while(iterator.hasNext()){
			String element = iterator.next();
			if(element.length()==3){
//				coll.remove(element);//错误的
				iterator.remove();
			}
		}
		System.out.println(coll);
	}

注意:不要在使用Iterator迭代器进行迭代时,调用Collection的remove(xx)方法,否则会报异常java.util.ConcurrentModificationException,或出现不确定行为。

3.3 增强for

增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。

格式:

for(元素的数据类型  变量 : Collection集合or数组){ 
  	//写操作代码
}

它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作

4 List集合

4.1 List接口介绍

java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List`接口的对象称为List集合。

List接口特点:

  1. List集合所有的元素是以一种线性方式进行存储的,例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)
  2. 它是一个元素存取有序的集合。即元素的存入顺序和取出顺序一致。
  3. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  4. 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

List集合类中元素有序、且可重复。这就像银行门口客服,给每一个来办理业务的客户分配序号:第一个来的是“张三”,客服给他分配的是0;第二个来的是“李四”,客服给他分配的1;以此类推,最后一个序号应该是“总人数-1”。
在这里插入图片描述

注意:
List集合关心元素是否有序,而不关心是否重复,请大家记住这个原则。例如“张三”可以领取两个号。

4.2 List接口中常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
1、添加元素

  • void add(int index, E ele)
  • boolean addAll(int index, Collection<? extends E> eles)

2、获取元素

  • E get(int index)
  • List subList(int fromIndex, int toIndex)

3、获取元素索引

  • int indexOf(Object obj)
  • int lastIndexOf(Object obj)

4、删除和替换元素

  • E remove(int index)
  • E set(int index, E ele)

List集合特有的方法都是跟索引相关:

public class ListDemo {
    public static void main(String[] args) {
		// 创建List集合对象
    	List<String> list = new ArrayList<String>();
    	
    	// 往 尾部添加 指定元素
    	list.add("图图");
    	list.add("小美");
    	list.add("不高兴");
    	
    	System.out.println(list);
    	// add(int index,String s) 往指定位置添加
    	list.add(1,"没头脑");
    	
    	System.out.println(list);
    	// String remove(int index) 删除指定位置元素  返回被删除元素
    	// 删除索引位置为2的元素 
    	System.out.println("删除索引位置为2的元素");
    	System.out.println(list.remove(2));
    	
    	System.out.println(list);
    	
    	// String set(int index,String s)
    	// 在指定位置 进行 元素替代(改) 
    	// 修改指定位置元素
    	list.set(0, "三毛");
    	System.out.println(list);
    	
    	// String get(int index)  获取指定位置元素
    	
    	// 跟size() 方法一起用  来 遍历的 
    	for(int i = 0;i<list.size();i++){
    		System.out.println(list.get(i));
    	}
    	//还可以使用增强for
    	for (String string : list) {
			System.out.println(string);
		}  	
	}
}

在JavaSE中List名称的类型有两个,一个是java.util.List集合接口,一个是java.awt.List图形界面的组件,别导错包了。

4.3 List的实现类

ArrayList集合
java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。

许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。

Vector集合
ArrayList与Vector的区别?

它们的底层物理结构都是数组,我们称为动态数组。

  • ArrayList是新版的动态数组,线程不安全,效率高,Vector是旧版的动态数组,线程安全,效率低。
  • 动态数组的扩容机制不同,ArrayList扩容为原来的1.5倍,Vector扩容增加为原来的2倍。
  • 数组的初始化容量,如果在构建ArrayList与Vector的集合对象时,没有显式指定初始化容量,那么Vector的内部数组的初始容量默认为10,而ArrayList在JDK1.6及之前的版本也是10,而JDK1.7之后的版本ArrayList初始化为长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。
  • Vector因为版本古老,支持Enumeration 迭代器。但是该迭代器不支持快速失败。而Iterator和ListIterator迭代器支持快速失败。如果在迭代器创建后的任意时间从结构上修改了向量(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。

5 Set集合

Set接口是Collection的子接口,set接口没有提供额外的方法。但是比Collection接口更加严格了。
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
Set集合支持的遍历方式和Collection集合一样:foreach和Iterator。
Set的常用实现类有:HashSet、TreeSet、LinkedHashSet。

5.1 HashSet

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。

java.util.HashSet底层的实现其实是一个java.util.HashMap支持,然后HashMap的底层物理实现是一个Hash表。(什么是哈希表,下一节在HashMap小节在细讲,这里先不展开)

HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能。HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。因此,存储到HashSet的元素要重写hashCode和equals方法。

示例代码:定义一个Employee类,该类包含属性:name, birthday,其中 birthday 为 MyDate类的对象;MyDate为自定义类型,包含年、月、日属性。要求 name和birthday一样的视为同一个员工。

public class Employee {
	private String name;
	private MyDate birthday;
	public Employee(String name, MyDate birthday) {
		super();
		this.name = name;
		this.birthday = birthday;
	}
	public Employee() {
		super();
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public MyDate getBirthday() {
		return birthday;
	}
	public void setBirthday(MyDate birthday) {
		this.birthday = birthday;
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((birthday == null) ? 0 : birthday.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Employee other = (Employee) obj;
		if (birthday == null) {
			if (other.birthday != null)
				return false;
		} else if (!birthday.equals(other.birthday))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	@Override
	public String toString() {
		return "姓名:" + name + ", 生日:" + birthday;
	}
}
public class MyDate {
	private int year;
	private int month;
	private int day;
	public MyDate(int year, int month, int day) {
		super();
		this.year = year;
		this.month = month;
		this.day = day;
	}
	public MyDate() {
		super();
	}
	public int getYear() {
		return year;
	}
	public void setYear(int year) {
		this.year = year;
	}
	public int getMonth() {
		return month;
	}
	public void setMonth(int month) {
		this.month = month;
	}
	public int getDay() {
		return day;
	}
	public void setDay(int day) {
		this.day = day;
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + day;
		result = prime * result + month;
		result = prime * result + year;
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		MyDate other = (MyDate) obj;
		if (day != other.day)
			return false;
		if (month != other.month)
			return false;
		if (year != other.year)
			return false;
		return true;
	}
	@Override
	public String toString() {
		return year + "-" + month + "-" + day;
	}
}
import java.util.HashSet;

public class TestHashSet {
	@SuppressWarnings("all")
	public static void main(String[] args) {
		HashSet<Employee> set = new HashSet<>();
		set.add(new Employee("张三", new MyDate(1990,1,1)));
		//重复元素无法添加,因为MyDate和Employee重写了hashCode和equals方法
		set.add(new Employee("张三", new MyDate(1990,1,1)));
		set.add(new Employee("李四", new MyDate(1992,2,2)));
		
		for (Employee object : set) {
			System.out.println(object);
		}
	}
}

5.2 LinkedHashSet

LinkedHashSet是HashSet的子类,它在HashSet的基础上,在结点中增加两个属性before和after维护了结点的前后添加顺序。java.util.LinkedHashSet,它是链表和哈希表组合的一个数据存储结构。LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。

LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("张三");
set.add("李四");
set.add("王五");
set.add("张三");
		
System.out.println("元素个数:" + set.size());
for (String name : set) {
	System.out.println(name);
}
运行结果:
元素个数:3
张三
李四
王五

5.3 TreeSet

底层结构:里面维护了一个TreeMap,都是基于红黑树实现的!

特点:
1、不允许重复
2、实现排序
自然排序或定制排序

如何实现去重的?

如果使用的是自然排序,则通过调用实现的compareTo方法
如果使用的是定制排序,则通过调用比较器的compare方法

如何排序?

方式一:自然排序
让待添加的元素类型实现Comparable接口,并重写compareTo方法
方式二:定制排序
创建Set对象时,指定Comparator比较器接口,并实现compare方法

自然顺序
如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值为0。

代码示例一:按照字符串Unicode编码值排序

@Test
	public void test1(){
		TreeSet<String> set = new TreeSet<>();
		set.add("zhangsan");  //String它实现了java.lang.Comparable接口
		set.add("lisi");
		set.add("wangwu");
		set.add("zhangsan");
				
		System.out.println("元素个数:" + set.size());
		for (String str : set) {
			System.out.println(str);
		}
	}

定制排序
如果放到TreeSet中的元素的自然排序(Comparable)规则不符合当前排序需求时,或者元素的类型没有实现Comparable接口。那么在创建TreeSet时,可以单独指定一个Comparator的对象。使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。

代码示例:学生类型未实现Comparable接口,单独指定Comparator比较器,按照学生的学号排序

public class Student{
	private int id;
	private String name;
	public Student(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	//......这里省略了name属性的get/set
	@Override
	public String toString() {
		return "Student [id=" + id + ", name=" + name + "]";
	}
}
@Test
	public void test3(){
		TreeSet<Student> set = new TreeSet(new Comparator<Student>(){

			@Override
			public int compare(Student o1, Student o2) {
				return o1.getId() - o2.getId();
			}
			
		});
		set.add(new Student(3,"张三"));
		set.add(new Student(1,"李四"));
		set.add(new Student(2,"王五"));
		set.add(new Student(3,"张三风"));
		
		System.out.println("元素个数:" + set.size());
		for (Student stu : set) {
			System.out.println(stu);
		}
	}

6 Map集合

我们通过查看Map接口描述,发现Map<K,V>接口下的集合与Collection<E>接口下的集合,它们存储数据的形式不同。

  • Collection中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
  • Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
  • Collection中的集合称为单列集合,Map中的集合称为双列集合。
  • 需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值(这个值可以是单个值,也可以是个数组或集合值)。
    在这里插入图片描述

6.1 Map常用方法

1、添加操作

  • V put(K key,V value)
  • void putAll(Map<? extends K,? extends V> m)

2、删除

  • void clear()
  • V remove(Object key)

3、元素查询的操作

  • V get(Object key)
  • boolean containsKey(Object key)
  • boolean containsValue(Object value)
  • boolean isEmpty()

4、元视图操作的方法:

  • Set keySet()
  • Collection values()
  • Set<Map.Entry<K,V>> entrySet()

5、其他方法

  • int size()
public class MapDemo {
    public static void main(String[] args) {
        //创建 map对象
        HashMap<String, String>  map = new HashMap<String, String>();

        //添加元素到集合
        map.put("黄晓明", "杨颖");
        map.put("文章", "马伊琍");
        map.put("邓超", "孙俪");
        System.out.println(map);

        //String remove(String key)
        System.out.println(map.remove("邓超"));
        System.out.println(map);

        // 想要查看 黄晓明的媳妇 是谁
        System.out.println(map.get("黄晓明"));
        System.out.println(map.get("邓超"));    
    }
}

使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;
若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

6.2 Map集合的遍历

Collection集合的遍历:(1)foreach(2)通过Iterator对象遍历

Map的遍历,不能支持foreach,因为Map接口没有继承java.lang.Iterable接口,也没有实现Iterator iterator()方法。只能用如下方式遍历:

(1)分开遍历:

  • 单独遍历所有key
  • 单独遍历所有value

(2)成对遍历:

  • 遍历的是映射关系Map.Entry类型的对象,Map.Entry是Map接口的内部接口。每一种Map内部有自己的Map.Entry的实现类。在Map中存储数据,实际上是将Key---->value的数据存储在Map.Entry接口的实例中,再在Map集合中插入Map.Entry的实例化对象,如图示:
public class TestMap {
	public static void main(String[] args) {
		HashMap<String,String> map = new HashMap<>();
		map.put("许仙", "白娘子");
		map.put("董永", "七仙女");
		map.put("牛郎", "织女");
		map.put("许仙", "小青");
		
		System.out.println("所有的key:");
		Set<String> keySet = map.keySet();
		for (String key : keySet) {
			System.out.println(key);
		}
		
		System.out.println("所有的value:");
		Collection<String> values = map.values();
		for (String value : values) {
			System.out.println(value);
		}
		
		System.out.println("所有的映射关系");
		Set<Map.Entry<String,String>> entrySet = map.entrySet();
		for (Map.Entry<String,String> entry : entrySet) {
//			System.out.println(entry);
			System.out.println(entry.getKey()+"->"+entry.getValue());
		}
	}
}

6.3 Map的实现类们

Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中HashMap是 Map 接口使用频率最高的实现类。

1、HashMap和Hashtable的区别与联系
HashMap和Hashtable都是哈希表。

HashMap和Hashtable判断两个 key 相等的标准是:两个 key 的hashCode 值相等,并且 equals() 方法也返回 true。因此,为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。

Hashtable是线程安全的,任何非 null 对象都可以用作键或值。

HashMap是线程不安全的,并允许使用 null 值和 null 键。

示例代码:添加员工姓名为key,薪资为value

	public static void main(String[] args) {
		HashMap<String,Double> map = new HashMap<>();
		map.put("张三", 10000.0);
		//key相同,新的value会覆盖原来的value
		//因为String重写了hashCode和equals方法
		map.put("张三", 12000.0);
		map.put("李四", 14000.0);
		//HashMap支持key和value为null值
		String name = null;
		Double salary = null;
		map.put(name, salary);
		
		Set<Entry<String, Double>> entrySet = map.entrySet();
		for (Entry<String, Double> entry : entrySet) {
			System.out.println(entry);
		}
	}

2、LinkedHashMap
LinkedHashMap 是 HashMap 的子类。此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。

示例代码:添加员工姓名为key,薪资为value

	public static void main(String[] args) {
		LinkedHashMap<String,Double> map = new LinkedHashMap<>();
		map.put("张三", 10000.0);
		//key相同,新的value会覆盖原来的value
		//因为String重写了hashCode和equals方法
		map.put("张三", 12000.0);
		map.put("李四", 14000.0);
		//HashMap支持key和value为null值
		String name = null;
		Double salary = null;
		map.put(name, salary);
		
		Set<Entry<String, Double>> entrySet = map.entrySet();
		for (Entry<String, Double> entry : entrySet) {
			System.out.println(entry);
		}
	}

3、TreeMap

基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

代码示例:添加员工姓名为key,薪资为value

package com.atguigu.map;

import java.util.Comparator;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.junit.Test;

public class TestTreeMap {
	@Test
	public void test1() {
		TreeMap<String,Integer> map = new TreeMap<>();
		map.put("Jack", 11000);
		map.put("Alice", 12000);
		map.put("zhangsan", 13000);
		map.put("baitao", 14000);
		map.put("Lucy", 15000);
		
		//String实现了Comparable接口,默认按照Unicode编码值排序
		Set<Entry<String, Integer>> entrySet = map.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
			System.out.println(entry);
		}
	}
	@Test
	public void test2() {
		//指定定制比较器Comparator,按照Unicode编码值排序,但是忽略大小写
		TreeMap<String,Integer> map = new TreeMap<>(new Comparator<String>() {

			@Override
			public int compare(String o1, String o2) {
				return o1.compareToIgnoreCase(o2);
			}
		});
		map.put("Jack", 11000);
		map.put("Alice", 12000);
		map.put("zhangsan", 13000);
		map.put("baitao", 14000);
		map.put("Lucy", 15000);
		
		Set<Entry<String, Integer>> entrySet = map.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
			System.out.println(entry);
		}
	}
}

4、Properties

Properties 类是 Hashtable 的子类,Properties 可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串。

存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法。

代码示例:

	public static void main(String[] args) {
		Properties properties = System.getProperties();
		String p2 = properties.getProperty("file.encoding");//当前源文件字符编码
		System.out.println(p2);
	}

八、IO流

1 流的分类

按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
按数据流的流向不同分为:输入流,输出流
在这里插入图片描述
Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

1.1 读文件步骤

无论是文本文件还是二进制文件,当需要读取文件数据时,需要完成以下步骤:

  1. 使用文件输入流打开指定文件:
    对于文本文件,应使用字符输入流FileReader流
    对于二进制文件,应使用字节输入流FileInputStream流
  2. 读取文件数据
  3. 关闭输入流

1.2 写文件步骤

无论是文本文件还是二进制文件,当需要将数据写入文件时,需要完成以下步骤:

  1. 使用文件输出流打开指定文件:
    对于文本文件,应使用字符输出流FileWriter流
    对于二进制文件,应使用字节输出流FileOutputStream流
  2. 将数据写入文件
  3. 关闭输出流

1.3 Reader 和 InputStream 是所有输入流的基类。

  • Reader(典型实现:FileReader)
    int read() // 读取一个字符
    int read(char [] c) //一次性读多个字符到缓冲区数组返回 值为有效字符.
    int read(char [] c, int off, int len)
  • InputStream(典型实现:FileInputStream)
    int read() //读取一个字节
    int read(byte[] b) //一次性读多个字节到缓冲区数组
    int read(byte[] b, int off, int len)

1.4 Writer & OutputStream 是所有输出流的基类。

Writer 和 OutputStream 也非常相似:
void write(int b/int c);
void write(byte[] b/char[] cbuf);
void write(byte[] b/char[] buff, int offset, int length);
void flush();
void close(); 需要先刷新,再关闭此流
因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作为参数
void write(String str);
void write(String str, int off, int len);

示例:

package com.atguigu.javase.io;

import org.junit.Test;

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

/**
 *              字节流             字符流
 *  输入流     InputStream         Reader(阅读器)
 *  输出流     OutputStream        Writer(写字器)
 *
 *  读写文件步骤 :
 *      1) 创建流对象, 建立通道
 *      2) 使用这个通道读写数据
 *          如果是要读 调用read()
 *          如果是要写 调用write(数据)
 *      3) 关闭流对象.
 */
public class IOTest {

    @Test
    public void test3() {
        // 1) 声明引用, 并赋值为null
        FileReader fileReader = null;
        // 2) try catch finally
        try {
            // 5) 创建流对象
            fileReader = new FileReader("一个文件");
            // 6) 处理数据
            int ch;
            while ((ch = fileReader.read()) != -1) {
                System.out.print((char)ch);
            }
        } catch (Exception e) {
            // 4) 在catch中处理异常
            e.printStackTrace();
        } finally {
            // 3) 在finally关闭流
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Test
    public void test2() {
        // 1) 读文本文件
        FileReader fr = null;
        try {
            fr = new FileReader("一个文件2");
            // 2)
            int ch;
            while ((ch = fr.read()) != -1) { // 读了一个字符, 需要不断的读, 直到末尾返回-1
                System.out.print((char) ch);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3)
            if (fr != null) {
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test1() throws Exception {
        // 1) 读文本文件
        FileReader fr = new FileReader("一个文件");
        // 2)
        int ch;
        while ((ch = fr.read()) != -1) { // 读了一个字符, 需要不断的读, 直到末尾返回-1
            System.out.print((char) ch);
        }
        // 3)
        fr.close();
    }
}

2 处理流之一:缓冲流

  • 为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组
  • 根据数据操作单位可以把缓冲流分为:
    BufferedReader 和 BufferedWriter
    BufferedInputStream 和 BufferedOutputStream
  • 缓冲流要“套接”在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,同时增加了一些新的方法, 增强了流处理能力.
  • 对于输出的缓冲流,写出的数据会先在内存中缓存,使用flush()将会使内存中的数据立刻写出
package com.atguigu.javase.io;

import org.junit.Test;

import javax.annotation.processing.Filer;
import java.io.*;
import java.util.Arrays;

/**
 * IO流 : 数据从一个结点流向另一个结点. 有方向性.
 *
 *              字节流                 字符流
 * 输入流      InputStream             Reader
 * 输出流      OutputStream            Writer
 *
 * FileInputStream  读二进制文件
 * FileReader 读文本文件
 * FileOutputStream 写二进制文件
 * FileWriter 写文本文件
 *
 * 处理文件
 * 1) 创建流对象, 和文件建立通道
 * 2) 通过流对象处理数据
 * 3) 关闭流
 *
 */
public class IOTest {

    @Test
    public void test8() {
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        ObjectInputStream ois = null;
        try {
            fis = new FileInputStream("二进制文件");
            bis = new BufferedInputStream(fis);
            ois = new ObjectInputStream(bis);

            int i = ois.readInt();
            boolean b1 = ois.readBoolean();
            boolean b2 = ois.readBoolean();
            long l = ois.readLong();
            double v = ois.readDouble();

            System.out.println(i);
            System.out.println(b1);
            System.out.println(b2);
            System.out.println(l);
            System.out.println(v);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test7() {
        // 二进制数据 : 在内存中如何 , 在文件中也如何
        FileOutputStream fos = null;
        BufferedOutputStream bos = null;
        ObjectOutputStream oos = null;
        try {
            fos = new FileOutputStream("二进制文件");
            bos = new BufferedOutputStream(fos);
            oos = new ObjectOutputStream(bos);

            oos.writeInt(20);
            oos.writeBoolean(true);
            oos.writeBoolean(false);
            oos.writeLong(30);
            oos.writeDouble(3.14);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test6() {
        FileWriter fileWriter = null;
        BufferedWriter bufferedWriter = null;
        try {
            fileWriter = new FileWriter("使用高级流写文件高级换行");
            bufferedWriter = new BufferedWriter(fileWriter);
            String[] content = {"234982374982374892374",
                                "aflkjasldkfja甲基叶酸三昧真火卷四地",
                                "我是一些内容, 请把我写到文件中吧1花飘万家雪",
                                "我是一些内容, 请把我写到文件中吧2sdf",
                                "我是一些内容, 请把我写到文件中吧3asdf",
                                "我是一些内容, 请把我写到文件中吧4",
                                "我是一些内容, 请把我写到文件中吧5",
                                "我是一些内容, 请把我写到文件中吧6s",
                                "我是一些内容, 请把我写到文件中吧7df",
                                "我是一些内容, 请把我写到文件中吧8"};
            for (int i = 0; i < content.length; i++) {
                String line = content[i];
                bufferedWriter.write(line);
                bufferedWriter.newLine(); // 最有价值方法, 可以写入跨平台的换行.
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedWriter != null) {
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test5() {
        FileReader fileReader = null;
        BufferedReader bufReader = null;
        int lineNumber = 1;
        try {
            fileReader = new FileReader("一个文件");
            bufReader = new BufferedReader(fileReader);
            String line;
            while ((line = bufReader.readLine()) != null) { // 最有价值方法
                System.out.println(lineNumber++ + " " + line); // line中没有换行符
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 只关闭高级流
            if (bufReader != null) {
                try {
                    bufReader.close(); // 自动关闭低级流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test4() {
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter("使用缓冲写文本");
            String[] content = {"234982374982374892374",
                                "aflkjasldkfja甲基叶酸三昧真火卷四地",
                                "我是一些内容, 请把我写到文件中吧1花飘万家雪",
                                "我是一些内容, 请把我写到文件中吧2sdf",
                                "我是一些内容, 请把我写到文件中吧3asdf",
                                "我是一些内容, 请把我写到文件中吧4",
                                "我是一些内容, 请把我写到文件中吧5",
                                "我是一些内容, 请把我写到文件中吧6s",
                                "我是一些内容, 请把我写到文件中吧7df",
                                "我是一些内容, 请把我写到文件中吧8"};
            for (int i = 0; i < content.length; i++) {
                String line = content[i];
                // 写字符数组
                char[] chars = line.toCharArray();
                //fileWriter.write(chars); // 写字符数组的所有字符
                // 这是超重点方法, 把数组的一部分写到输出流中.
                fileWriter.write(chars, 8, chars.length - 8); // 第2个参数的作用是控制开始下标, 第3个参数是要写多少个字符
                fileWriter.write(13);
                fileWriter.write(10);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fileWriter != null) {
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test3() {
        int lineNumber = 1;
        System.out.print(lineNumber++ + " ");
        FileReader fileReader = null;
        try {
            fileReader = new FileReader("一个文件");
            char[] buf = new char[100];
            //int count = fileReader.read(buf); // 一次性读批量个字符到缓冲区中, 返回值的含义是实际读到数组中的字符数
            int count; // 记录实际读的字符数
            while ((count = fileReader.read(buf)) != -1) {
                // 处理已经读到的数据, 必须要以实际读到的为准.
                for (int i = 0; i < count; i++) {
                    System.out.print(buf[i]);
                    if (buf[i] == 10) {
                        System.out.print(lineNumber++ + " ");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test2() {
        FileWriter fileWriter = null;
        try { // ctr + alt + t => A
            fileWriter = new FileWriter("写一个文本"); // 如果文件不存在, 则会自动创建, 如果文件存在, 里面的内容全部清空
            fileWriter.write('a');
            fileWriter.write('b');
            fileWriter.write('c');
            fileWriter.write(13);
            fileWriter.write(10);
            fileWriter.write('1');
            fileWriter.write('3');
            fileWriter.write('4');
            fileWriter.write('5');
            fileWriter.write(13);
            fileWriter.write('\n');
            fileWriter.write('我');
            fileWriter.write('是');
            fileWriter.write('汉');
            fileWriter.write('字');
            fileWriter.write(13);
            fileWriter.write(10);
            fileWriter.write('x');
            fileWriter.write('y');
            fileWriter.write('z');
            fileWriter.write('\r');
            fileWriter.write(10);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fileWriter != null) {
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 读一个文本文件, 在输出时加上行号
    public static void main(String[] args) {
        int lineNumber = 1;
        System.out.print(lineNumber++ + " ");
        // 1) 声明引用 并赋值为null
        FileReader fileReader = null;
        // 2) try catch finally
        try {
            // 5) 在try中创建流对象
            fileReader = new FileReader("一个文件");
            // 6) 处理数据
            int ch; // 声明变量, 用于接收读到的字符的码值
            while ((ch = fileReader.read()) != -1) { // read()方法会从流中读一个字符, 并返回字符的码值
                // 处理已经读的数据
                System.out.print((char)ch);
                if (ch == 10) {
                    System.out.print(lineNumber++ + " ");
                }

            }
        } catch (Exception e) {
            // 4) 在catch中处理异常
            e.printStackTrace();
        } finally {
            // 3) 释放资源
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test1() {
        // 1) 声明引用 并赋值为null
        FileReader fileReader = null;
        // 2) try catch finally
        try {
            // 5) 在try中创建流对象
            fileReader = new FileReader("一个文件");
            // 6) 处理数据
            int ch; // 声明变量, 用于接收读到的字符的码值
            while ((ch = fileReader.read()) != -1) { // read()方法会从流中读一个字符, 并返回字符的码值
                // 处理已经读的数据
                System.out.print((char)ch);
            }
        } catch (Exception e) {
            // 4) 在catch中处理异常
            e.printStackTrace();
        } finally {
            // 3) 释放资源
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

package com.atguigu.javase.io;

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

public class FileCopy {

    //编写程序FileCopy.java,在测试方法中,将FileCopy.java复制为FileCopy.java.bak文件;
    //查看FileCopy.java.bak文件的内容,验证复制是否正确。
    public static void main(String[] args) {
        FileReader fileReader = null;
        FileWriter fileWriter = null;
        try {
            //fileReader = new FileReader(".\\src\\com\\atguigu\\javase\\io\\FileCopy.java");
            fileReader = new FileReader("解放军进行曲.mp3");
            fileWriter = new FileWriter("解放军跑步曲.mp3");
            char[] buf = new char[200];
            int count; // 记录实际读到的字符数
            while ((count = fileReader.read(buf)) != -1) { // 不断地读文件, 直到-1为止
                // 处理已经读到的数据
                fileWriter.write(buf, 0, count);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fileWriter != null) {
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3 处理流之二:转换流

  • InputStreamReader

用于将字节流中读取到的字节按指定字符集解码成字符。需要和InputStream“套接”。

构造方法
public InputStreamReader(InputStream in)
public InputSreamReader(InputStream in,String charsetName)

  • OutputStreamWriter

用于将要写入到字节流中的字符按指定字符集编码成字节。需要和OutputStream“套接”。
构造方法
public OutputStreamWriter(OutputStream out)
public OutputSreamWriter(OutputStream out,String charsetName)
在这里插入图片描述

4 处理流之三:标准输入输出流

  • System.in和System.out分别代表了系统标准的输入和输出设备
  • 默认输入设备是键盘,输出设备是显示器
  • System.in的类型是InputStream
  • System.out的类型是PrintStream,其是OutputStream的子类FilterOutputStream 的子类
  • 通过System类的setIn,setOut方法对默认设备进行改变。
    public static void setIn(InputStream in)
    public static void setOut(PrintStream out)
package com.atguigu.javase.io;

import jdk.internal.util.xml.impl.Input;
import org.junit.Test;

import java.io.*;
import java.util.Scanner;

/**
 * 基本流 : Reader, Writer, InputStream, OutputStream
 * 文件流 : FileInputStream, FileOutputStream
 * 缓冲流 : BufferedReader, BufferedWriter
 * 对象流 : ObjectInputStream, ObjectOutputStream
 * 转换流 : InputStreamReader, OutputStreamWriter
 *
 * 对象序列化 : 把对象数据写入输出流. writeObject
 * 反序列化 : 把输入流中的数据还原成对象. readObject
 *
 * 对于输出流, 一定要执行flush, 这样才能保证数据写入硬盘. 当执行close时也会自动flush
 * 如果要以追加方式写文件, 必须在创建结点流时传入第2个参数 为true
 */

// 对象要想序列化, 类要实现Serializable
class Student implements Serializable {

    //public static final long serialVersionUID = 2; 控制版本

    // 静态属性无法序列化
    public static String school = "atguigu";

    private int id;
    private String name;
    private int grade;
    private transient double score; // transient修饰的成员不可以序列化

    public Student() {}

    public Student(int id, String name, int grade, double score) {
        this.id = id;
        this.name = name;
        this.grade = grade;
        this.score = score;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getGrade() {
        return grade;
    }

    public void setGrade(int grade) {
        this.grade = grade;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", grade=" + grade +
                ", score=" + score +
                '}';
    }
}

// Person类, 属性name, age, height
// 创建3个对象, 把它们序列化到文件中. 分析文件
// 再写程序反序列化.
public class IOTest {

    /**
     * 获取目录大小
     * @param dir
     * @return
     */
    public long dirSize(File dir) {
        long size = 0;
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file1 : files) {
                if (file1.isFile()) {
                    size += file1.length(); // 只有file对象的.length方法有效
                } else if (file1.isDirectory()) {
                    size += dirSize(file1);
                }
            }
        }
        return size;
    }

    // 练习 : 统计目录C:/windows大小
    @Test
    public void test13() {
        File file = new File("c:/windows");
        long l = dirSize(file);
        System.out.println(l);
    }

    @Test
    public void test12() throws IOException {
        File file1 = new File("d:/Mywork");
        System.out.println(file1.length());

        File file = new File("aaa/bbb/ccc/ddd/eee");
        //file.createNewFile(); // 文件大小为0
        //file.mkdir(); // 创建目录
        file.mkdirs(); // 创建多层目录

        // 最重要的方法:
        File[] files = file.listFiles();// 列出目录下的所有文件对象(包含 子目录和子文件)
        long size = 0;
        for (int i = 0; i < files.length; i++) {
            if (files[i].isFile()) {
                System.out.println("文件 : " + files[i]);
            } else if (files[i].isDirectory()) {
                System.out.println("目录 : " + files[i]);
            }
            size += files[i].length();
        }
        System.out.println("size = " + size);
    }
    @Test
    public void test11() throws IOException {
        File file = new File("对象序列化");
        System.out.println("file.canExecute() : " + file.canExecute()); 
        System.out.println("file.getName() : " + file.getName());
        System.out.println("file.length() : " + file.length());
        System.out.println("file.getAbsolutePath() : " + file.getAbsolutePath());
        System.out.println("file.getCanonicalPath() : " + file.getCanonicalPath());
        System.out.println("file.canRead() : " + file.canRead());
        System.out.println("file.canWrite() : " + file.canWrite());
        //System.out.println("file.createNewFile() : " + file.createNewFile());
        System.out.println("file.exists() : " + file.exists());
        System.out.println("file.getFreeSpace() : " + file.getFreeSpace());
        System.out.println("file.getTotalSpace() : " + file.getTotalSpace());
        System.out.println("file.isDirectory() : " + file.isDirectory());
        System.out.println("file.isFile() : " + file.isFile());
        System.out.println("file.lastModified() : " + file.lastModified());
        //System.out.println("file.delete(`) : " + file.delete());
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            if (scanner.hasNextInt()) {
                int i = scanner.nextInt();
                System.out.println("整数 : " + i);
            } else if (scanner.hasNextDouble()) {
                double v = scanner.nextDouble();
                System.out.println("浮点数 : " + v);
            } else {
                String next = scanner.next();
                System.out.println("普通串 : " + next);
            }
        }
        scanner.close();
    }

    // 练习 : 从键盘获取一些内容, 把内容再写到一个文件key_gbk.txt, 以GBK编码方式写文本文件
    // 直到从键盘输入了exit或quit命令.
    public static void main3(String[] args) {
        InputStream in = System.in;
        InputStreamReader isr = null;
        BufferedReader bufferedReader = null;

        FileOutputStream fos = null;
        OutputStreamWriter osw = null;
        BufferedWriter bufferedWriter = null;

        try {
            isr = new InputStreamReader(in);
            bufferedReader = new BufferedReader(isr);

            fos = new FileOutputStream("key_gkb.txt");
            osw = new OutputStreamWriter(fos, "gbk");
            bufferedWriter = new BufferedWriter(osw);

            String line;
            while ((line = bufferedReader.readLine()) != null) {
                if (line.equalsIgnoreCase("exit") || line.equalsIgnoreCase("quit")) {
                    break;
                }
                bufferedWriter.write(line);
                bufferedWriter.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (bufferedWriter != null) {
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main2(String[] args) {
        InputStream in = System.in;
        InputStreamReader isr = null;
        BufferedReader bufferedReader = null;
        try {
            isr = new InputStreamReader(in);
            bufferedReader = new BufferedReader(isr);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                if (line.equals("exit")) {
                    break;
                }
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test10() {
        System.out.println("hello");
        System.out.flush();
        //System.out.close();
        System.out.println("jllkjkj");
    }

    @Test
    public void test9() {
        FileOutputStream fos = null;
        OutputStreamWriter osw = null;
        BufferedWriter bufferedWriter = null;
        try {
            fos = new FileOutputStream("普通文本文件_gbk.txt");
            osw = new OutputStreamWriter(fos, "gbk");
            bufferedWriter = new BufferedWriter(osw);

            bufferedWriter.write("三昧真火abc");
            bufferedWriter.newLine();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedWriter != null) {
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test8() {
        FileWriter fileWriter = null;
        BufferedWriter bufferedWriter = null;
        try {
            fileWriter = new FileWriter("普通文本文件.txt", true);
            bufferedWriter = new BufferedWriter(fileWriter);

            bufferedWriter.write("三昧真火abc");
            bufferedWriter.newLine();

            bufferedWriter.flush(); // 强制把缓冲区中的数据刷入硬盘.

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedWriter != null) {
                try {
                    bufferedWriter.close(); // 关闭流时, 会自动flush
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 使用转换流处理特定的编码的文本文件
    @Test
    public void test7() {
        FileInputStream fis = null;
        InputStreamReader isr = null;
        BufferedReader bufferedReader = null;
        try {
            fis = new FileInputStream("HashMap2.java");
            isr = new InputStreamReader(fis, "gbk"); // 在转换流进行转换时, 要依据指定的编码方式解码文件到字符串.
            bufferedReader = new BufferedReader(isr);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test6() {
        FileReader fileReader = null; // FileReader只能处理和项目一致的编码的文本.
        BufferedReader bufferedReader = null;
        try {
            fileReader = new FileReader("HashMap2.java");
            bufferedReader = new BufferedReader(fileReader);
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test5() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("对象序列化"));
            /*
            Object o1 = ois.readObject();
            Object o2 = ois.readObject();
            Object o3 = ois.readObject();
            System.out.println(o1);
            System.out.println(o2);
            System.out.println(o3);

            System.out.println(((Student)o1).school);
             */
            Object o = ois.readObject();
            Student[] arr = (Student[]) o;
            for (Student student : arr) {
                System.out.println(student);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test4() {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("对象序列化"));

            Student s1 = new Student(1, "小明", 3, 80);
            Student s2 = new Student(2, "小丽", 6, 70);
            Student s3 = new Student(3, "小刚", 5, 30);

            s1.school = "尚硅谷";

            //oos.writeObject(s1);
            //oos.writeObject(s2);
            //oos.writeObject(s3);
            Student[] arr = {s1, s2, s3};
            oos.writeObject(arr);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test3() throws UnsupportedEncodingException {
        // utf8是编码方式
        // 编码 : 把字符串转换成byte[], 目的是把数据写文件或网络传输
        String s = "abc我和你yyy";
        //byte[] bytes1 = s.getBytes(); //utf8, 使用项目默认编码
        byte[] bytes1 = s.getBytes("utf8"); //utf8
        for (int i = 0; i < bytes1.length; i++) {
            System.out.print(Integer.toHexString(bytes1[i]) + " ");
        }
        System.out.println();
        byte[] bytes2 = s.getBytes("gbk");
        for (int i = 0; i < bytes2.length; i++) {
            System.out.print(Integer.toHexString(bytes2[i]) + " ");
        }
        System.out.println();
        // 解码 : byte[] 还原成字符串, 把文件中的数据或网络中的数据还原成字符串
        //String s1 = new String(bytes1);
        String s1 = new String(bytes1, "utf8"); // 以指定的UTF8编码方式进行解码
        System.out.println(s1);

        String s2 = new String(bytes2, "gbk"); // 以指定的GBK编码方式进行解码, 但是码值到unicode码还需要再转换一下 GBK => Unicode
        System.out.println(s2);
    }

    @Test
    public void test2() {
        int n1 = 0x6211;
        int n2 = 0x548C;
        System.out.println(n1);
        System.out.println(n2);
        System.out.println((char)n1);
        System.out.println((char)n2);

        int n3 = 0xCED2;
        System.out.println(n3);
    }

    @Test
    public void test1() {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("二进制文件"));
            oos.writeByte(10);
            oos.writeShort(20);
            oos.writeInt(30);
            oos.writeLong(40);
            oos.writeFloat(50.0f);
            oos.writeDouble(60.0);
            oos.writeChar('我');
            oos.writeBoolean(true);

            oos.writeUTF("abc我和你yyy"); // 以UTF8编码方式写这个字符串.
            oos.writeChars("abc我和你yyy"); // 写字符数组.
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

九 网络编程

9.1 网络编程三要素

9.1.1、协议

  • **协议:**计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。

9.1.2、IP地址

IP地址:指互联网协议地址(Internet Protocol Address),俗称IP地址。它用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

IP地址分类方式一:

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。我们现在的手机就是IPv6地址。

IP地址分类方式二:

公网地址( 万维网使用)和 私有地址( 局域网使用)。192.168.开头的就是私有址址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用

特殊的IP地址:

  • 本地回环地址(hostAddress):127.0.0.1
  • 主机名(hostName):localhost

常用命令:

  • 查看本机IP地址,在控制台输入:
ipconfig
ipconfig /all    
  • 检查网络是否连通,在控制台输入:
ping 空格 IP地址
ping 220.181.57.216

域名:

因为IP地址数字不便于记忆,因此出现了域名,域名容易记忆,当在连接网络时输入一个域名地址后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 ------- 域名解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3Y11IQ8-1646470582192)(imgs/1564021975715.png)]

9.1.3、端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

  • 端口号:用两个字节表示的整数,它的取值范围是0~65535
    • 公认端口:0~1023。被预先定义的服务通信占用,如:HTTP(80),FTP(21),Telnet(23)
    • 注册端口:1024~49151。分配给用户进程或应用程序。如:Tomcat(8080),MySQL(3306),Oracle(1521)。
    • 动态/私有端口:49152~65535。

如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

9.2 InetAddress类

InetAddress类用来表示IP地址,Internet上的主机有两种方式表示地址:

  • 域名地址(hostName):www.atguigu.com
  • IP 地址(hostAddress):202.108.35.210

lInetAddress 类没有提供公共的构造器,而是提供了如下几个静态方法来获取InetAddress 实例

  • public static InetAddress getLocalHost():返回本地主机的IP地址
  • public static InetAddress getByName(String host):根据给定的主机名、域名地址、IP地址,返回对应的IP地址。

InetAddress 提供了如下几个常用的方法

  • public String getHostAddress() :以文本形式返回IP地址字符串。
  • public String getHostName() :以文本形式返回IP地址的主机名
package com.caojie.net.demo;

import org.junit.Test;

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

/**
 * InetAddress用来表示IP地址
 */
public class InetAddressTest {
    /**
     * 获取本地主机地址InetAddress,包括IP地址和主机名
     */
    @Test
    public void getLocalHostTest() {
        try {
            // 该方法声明了一个UnknownHostException异常,所以我们使用try catch抓住该异常
            InetAddress localHost = InetAddress.getLocalHost();
            // 获取本地主机的IP地址
            String hostAddress = localHost.getHostAddress();
            // 192.168.1.23
            System.out.println(hostAddress);
            // 获取主机名称
            String hostName = localHost.getHostName();
            // caojie
            System.out.println(hostName);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据计算机主机名获取InetAddress
     */
    @Test
    public void getAddressTest() {
        try {
            // 根据计算机主机名称
            InetAddress address = InetAddress.getByName("caojie");
            // caojie/192.168.1.23
            System.out.println(address);
            // 以字符串形式获取IP地址
            String hostAddress = address.getHostAddress();
            System.out.println(hostAddress);
            // 以字符串形式获取主机名
            String hostName = address.getHostName();
            // caojie
            System.out.println(hostName);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}

9.3 Socket(重点)

Socket叫做“套接字”。是多台计算机之间通信的端点,能够在多个主机之间也可以在主机内部进行通信。Socket可以分为:流套接字数据报套接字
流套接字**(stream socket):封装了TCP协议的套接字

  • ServerSocket:TCP服务器套接字。服务器套接字等待客户端套接字的连接。
  • Socket:TCP客户端套接字(也可以就叫“套接字”)。是两台机器间通信的端点。

下图是生活中的套接字:
在这里插入图片描述

  • 数据报套接字(datagram socket):封装了UDP协议的套接字
    • DatagramSocket:此类表示用来发送和接收UDP数据报包的套接字。

9.4 TCP网络编程(重点)

9.4.1 通信模型

TCP编程分为服务器端编程和客户端编程,其通信模型如图所示:
在这里插入图片描述

总体分为三步:

  1. 建立连接(客户端和服务器建立连接)
  2. 开始通信(客户端服务器使用IO流进行数据传输)
  3. 结束通信(关闭资源),JDK7提供了 try with resources 弱化了这一步

1、服务器端
服务器程序的工作过程包含以下步骤:

  • 调用ServerSocket(int port)构造方法:创建一个服务器端套接字对象。
  • 调用ServerSocket的accept()方法 :等待并接受客户端连接请求,并返回客户端套接字(Socket)对象。
  • 调用Socket的getInputStream 和getOutputStream () :获取输入流和输出流,开始数据的发送和接收。

9.4.2 客户端

客户端程序的工作过程包含以下步骤:

  • 调用Socket(String address,int port)构造方法:创建一个客户端套接字对象。
  • 调用Socket的getInputStream 和getOutputStream () :获取输入流和输出流,开始数据的发送和接收。

9.4.3 相关API

ServerSocket类的构造方法:

  • ServerSocket(int port) :创建绑定到特定端口的服务器套接字。

ServerSocket类的常用方法:

  • Socket accept():侦听并接受到客户端套接字的连接。

Socket类的常用构造方法

  • public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
  • public Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口号。

Socket类的常用方法

头两个方法是重点,必须掌握

  • public InputStream getInputStream():返回此套接字的输入流,可以用于接收消息
  • public OutputStream getOutputStream():返回此套接字的输出流,可以用于发送消息
  • public InetAddress getInetAddress():此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
  • public InetAddress getLocalAddress():获取套接字绑定的本地地址。
  • public int getPort():此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。
  • public int getLocalPort():返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回 -1。
  • public void close():关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。
  • public void shutdownInput():如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
  • public void shutdownOutput():禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。
    示例代码:
package com.atguigu.socket1;

import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

/**
 * 案例:客户端连接服务器,连接成功后,服务器向客户端发送系统当前时间
 * 步骤:
 * 服务器端步骤如下:
 * 1.创建服务器端套接字对象ServerSocket并绑定端口号(61671)
 * 2.调用ServerSocket的accept方法监听(等待)客户端的连接并返回客户端套接字对象Socket
 * 3.一旦有客户端套发起连接,就获取客户端套接字的输出管道
 * 4.调用输出管道的write方法向客户端发送系统当前日期和时间
 * 5.调用输出管道的flush方法强制把数据刷新到客户端
   小结:
   	1 ServerSocket ss = new ServerSocket(61671);
   		不管有多少个客户端连接,服务器套接字只创建一次
      
    2 Socket socket = ss.accept();
    	服务器等待客户端的请求,如果没有客户端连接服务器,服务器会一直阻塞
    3 OutputStream out = socket.getOutputStream();
    	获取套接字输出流对象,用于向客户端发送数据。它是一个基础流处理数据效率不高,需要使用一个装饰流对它进行装饰
    4  BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
    	装饰流装饰了基础流    	
 */
public class Server {
    public static void main(String[] args) {
        System.out.println("==========服务器套接字===========");
        try(
                // 创建服务器端套接字,并绑定61671端口
                ServerSocket ss = new ServerSocket(61671);
                Socket socket = ss.accept();
                OutputStream out = socket.getOutputStream();
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
                ) {
            Date date = new Date();
            bw.write(date.toString());
            // 向客户端发送换行符号,表示服务器发送结束了
            bw.newLine();
            bw.flush();
        }catch(Exception e){
            System.err.println("服务器套接字连接失败");
            e.printStackTrace();
        }
    }
}

客户端示例代码

package com.atguigu.socket1;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
 * 案例:客户端套接字向服务器发起连接请求,接收服务器发送的系统当前时间
 * 步骤:
 	1 创建客户端套接字Socket对象,绑定服务器IP地址和服务器端口61671.
    	注意:服务器端口和客户端端口必须一致
 	2 调用Socket的getInputStream方法获取客户端套接字的输入管道,接收服务器发送的数据
 	3 调用输入管道的readLine()方法接收服务器发送的数据(当前时间)
 	4 打印当前时间
  小结:
  	1 Socket socket = new Socket("192.168.1.125",61671);
  	  客户端每次向服务器发起一次连接都会创建一次socket对象
  	2 InputStream in = socket.getInputStream();
  	  获取套接字的输入流,用于接受服务器发送的数据,InputStream是一个基础流,读取数据效率不高所以需要使用装饰流来装饰它
  	3 BufferedReader br  = new BufferedReader(new InputStreamReader(in));
  	  BufferedReader是一个基于字符的缓冲流,用来装饰基础流
  	4 br.readLine() 接受服务器发送的数据
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("-------客户端套接字-------");
        try(
                Socket socket = new Socket("192.168.1.125",61671);
                InputStream in = socket.getInputStream();
                BufferedReader br  = new BufferedReader(new InputStreamReader(in));
                ) {
            String data = br.readLine();
            System.out.println(data);
        } catch(Exception e) {
            System.err.println("客户端连接失败"+e.getMessage());
            e.printStackTrace();
        }
    }
}

十 反射(Reflect)

10.1 反射概念

Java的入射就是将字节码文件使用类加载器加载到JVM并生成Class对象的过程。什么是反射?程序在运行期间动态获取Class对象信息(属性信息、方法信息、构造方法信息)、创建对象、调用方法的过程叫做反射。反射能够让你的程序具备高度的灵活性。我们后面学习HADOOP分布式架构会大量使用反射。

10.2 javalang.Class类

要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关API(1)java.lang.Class(2)java.lang.reflect.*。所以,Class对象是反射的根源(入口)。
1、哪些类型可以获取Class对象

所有Java类型用代码示例

//(1)基本数据类型和void
例如:int.class
	 void.class
//(2)类和接口
例如:String.class
	Comparable.class
//(3)枚举
例如:ElementType.class
//(4)注解
例如:Override.class
//(5)数组
例如:int[].class

10.2.1 获取Class对象的四种方式(重点)

名称.class、
对象.getClass()、
Class.forName(类型全名称) 、
ClassLoader的类加载器对象.loadClass(“类型全名称”)。

(1)类型名称.class

要求编译期间已知类型

package com.atguigu.reflect4;

public class Student {
}
package com.atguigu.reflect4;

import org.junit.Test;

/**
 反射的入口,就是获取类字节码对应的Class对象。
 使用四种方式获取Student的Class对象
 */
public class ClassTest {
    /**
     * 1 类型名称.class
     */
    @Test
    public void clazzTest1() {
        // 在编译期已知类型是Student,所以在接收的时候可以指定明确的类型
        Class<Student> clazz = Student.class;
        // class com.atguigu.reflect4.Student
        System.out.println(clazz);
    }
}

(2)对象.getClass()

获取对象的运行时类型

    /**
     * 2 对象名称.getClass()
     */
    @Test
    public void clazzTest2() {
        Student stu = new Student();
        // ?可以是Student类型,也可以是Student的子类
        Class<? extends Student> clazz = stu.getClass();
        // class com.atguigu.reflect4.Student
        System.out.println(clazz);
    }

(3)Class.forName(类型全名称) 常用

可以获取编译期间未知的类型

    /**
     * 3 Class.forName("类型全名称")
     */
    @Test 
    public void clazzTest3() {
        try {
            // 在程序运行期间"类型全名称"在方法区中找不到就会抛出ClassNotFoundException
            // <?>表示未知的类型
            Class<?> clazz = Class.forName("com.atguigu.reflect4.Student");
            System.out.println(clazz);
        } catch (Exception e) {
            System.err.println("获取Student Class失败");
            e.printStackTrace();
        }
    }

(4)ClassLoader的类加载器对象.loadClass(类型全名称)

可以用系统类加载对象或自定义加载器对象加载指定路径下的类型

    /**
     * 4 类加载器对象.loadClass("类型全名称");
     * 步骤:
     *  1 获取Student的类加载器对象
     *  2 调用类加载器对象的loadClass("类型全名称")方法获取Student对应的Class对象
     */
    @Test
    public void clazzTest4() {
        // 获取类加载器对象
        ClassLoader classLoader = Student.class.getClassLoader();
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(classLoader);
        try {
            Class<?> clazz = classLoader.loadClass("com.atguigu.reflect4.Student");
            // class com.atguigu.reflect4.Student
            System.out.println(clazz);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

类型的Class对象,在方法区中只有一份,或者说字节码文件加载到JVM只执行一次。

 /**
     * 使用两种方式获取Student的Class对象,判断是否相等
     * 小结:类型的Class对象,在方法区中只有一份
     */
    @Test
    public void classEqualsTest(){
        Class<Student> clazz = Student.class;
        try {
            Class<?> _class = Class.forName("com.atguigu.reflect4.Student");
            // 打印结果:true
            // 类型的Class对象,在方法区中只有一份
            System.out.println(clazz == _class);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

10.3 反射的应用(重点)

可以获取包信息、 获取类型修饰符信息、获取类型名称信息、获取父类信息、获取实现接口信息、属性信息、构造方法信息、成员方法信息。可以使用反射创建对象、调用成员方法、暴力破解等操作。
Class对象常用的方法:

方法名称方法描述
getDeclaredFields()获取当前Class对象所有的属性,但是不包括父类的属性
getDeclaredField(name)根据名称获取Class对象的某一个属性
getFields()获取当前Class对象所有的public属性,包括父类的public属性
getField()根据名称获取Class对象的某一个公有属性
forName(name)静态方法 反射入口,用来加载某个类型
getPackage()获取当前Class对象的包信息
getModifiers()获取当前Class对象的访问修饰符
getName()获取当前Class对象的字符串表示的名称(包名称+类名称)
getSimpleName()获取当前Class对象的字符串表示的名称(类名称)
isInterface()获取当前Class对象是否为接口,true是接口 false不是接口
getSuperClass()获取当前Class对象父类Class对象
getInterfaces()获取当前Class对象实现的所有接口
getDeclaredMethods()获取当前Class对象的所有方法签名
getDeclaredMethod(name)根据参数名称获取当前Class对象的某一个方法签名
getDeclaredConstructors()获取当前Class对象的所有构造器签名
getDeclaredConstructor(params)根据参数名称获取指定的构造器签名

10.3.1 获取Class对象的包信息

package com.atguigu.reflect6;

import org.junit.Test;

import java.util.ArrayList;

/**
 * 使用反射获取ArrayList成员信息
 */
public class ArrayListReflectClass {
    /**
     * 获取包信息
     * 步骤:
     * 1 使用反射获取ArrayList的Class对象
     * 2 调用Class对象的getPackage方法获取包信息
     * 3 调用getName()方法获取包名称
     */
    @Test
    public void getPackageTest() {
        Class<ArrayList> clazz = ArrayList.class;
        Package pck = clazz.getPackage();
        // java.util
        String packageName = pck.getName();
        System.out.println(packageName);
    }
}

10.3.2 获取Class对象的类型信息

​ 可以在程序运行期间动态获取包信息、 获取类型修饰符信息、获取类型名称信息、获取父类信息、获取实现接口信息。

​ 获取如下信息:

package java.utils;
public class ArrayList extends AbstractList implements List,Serialiazable{
    
}
package com.atguigu.reflect6;

import org.junit.Test;

import java.lang.reflect.Modifier;
import java.util.ArrayList;

/**
 * 使用反射获取ArrayList成员信息
 */
public class ArrayListReflectClass {
    /**
     * 反射的入口,clazz表示ArrayList的Class对象
     */
    private Class<ArrayList> clazz = ArrayList.class;
    /**
     * 获取包信息
     * 步骤:
     * 1 使用反射获取ArrayList的Class对象
     * 2 调用Class对象的getPackage方法获取包信息
     * 3 调用getName()方法获取包名称
     */
    @Test
    public void getPackageTest() {
        // pck表示ArrayList的包信息
        Package pck = clazz.getPackage();
        // 获取包名称
        // java.util
        String packageName = pck.getName();
        System.out.println(packageName);
    }

    /**
     * 获取类型的修饰符
     * 步骤:获取ArrayList类型对应的修饰符并
     */
    @Test
    public void getModifierTest() {
        // 返回此类或接口的Java语言修饰符,用整数编码
        int mod = clazz.getModifiers();
        // 将修饰符转换为字符串
        String modifier = Modifier.toString(mod);
        System.out.println(modifier);
    }

    /**
     * 获取ArrayList修饰的类型 interface还是class
     */
    @Test
    public void getTypeTest(){
        // 判断clazz是否是接口类型,true是接口,false不是接口
        String typeName = clazz.isInterface() ? "interface" : "class";
        System.out.println(typeName);
    }

    /**
     * 获取ArrayList父类型Class信息
     */
    @Test
    public void getSuperTest() {
        // ?可以是ArrayList类型,也可以是ArrayList父类型
        // 获取ArrayList父类的Class对象
        Class<? super ArrayList> superClazz = clazz.getSuperclass();
        // 返回父类的简单名称(此时没有包名称)
        String superName = superClazz.getSimpleName();
        // AbstractList
        System.out.println(superName);
    }

    /**
     * 获取ArrayList实现的接口信息
     * 步骤:
     *  1 调用getInterfaces()方法,获取ArrayList实现的接口信息,并返回一个数组
     *  2 逐个遍历数组每个接口信息
     *  4 调用getSimpleName()方法获取接口信息的名称
     *  4 打印接口名称
     */
    @Test
    public void getArrayListInterfaceTest() {
        // 获取ArrayList实现的接口信息
        Class<?>[] inters = clazz.getInterfaces();
        for (Class<?> inter : inters) {
            String simpleName = inter.getSimpleName();
            System.out.print(simpleName+" ");
        }
    }
}

10.3.3 获取Class对象的属性(Field)信息

Field表示Class对象的字段信息,每个对象的属性在Class对象中都对应一个Field。

Field对象常用的方法

方法名称方法描述
setAccessible(boolean)设置Field访问的可见性,true可见
set(obj,value)暴力破解设置Field属性签名的实际值,参数1:属性所属的对象,参数2:实际值
getName()获取属性签名的全名
getSimpleName()获取属性签名的名称
getModifiers()获取当前Field对象的修饰符

获取如下属性信息:

private static final long serialVersionUID
private static final int DEFAULT_CAPACITY
private static final Object[] EMPTY_ELEMENTDATA
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA
transient Object[] elementData
private int size
private static final int MAX_ARRAY_SIZE

代码如下:

package com.atguigu.reflect6;

import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

/**
 * 获取ArrayList所有的属性签名信息
 */
public class ArrayListReflectClass2 {

    /**
     * 获取ArrayList所有的属性信息(Field)
     * 步骤:
     *   1 调用Class对象的getDeclaredFields方法获取所有的属性信息
     *   2 逐个遍历每个属性信息对象(Field)
     *   3 调用Field的成员方法,获取属性修饰符、属性数据类型、属性名称
     *   4 打印属性信息
     */
    @Test
    public void getAllFieldsTest() {
        Class<ArrayList> clazz = ArrayList.class;
        // 获取ArrayList所有的属性信息,包括公有、私有、保护、默认。但是不包括父类的属性信息
        Field[] fields = clazz.getDeclaredFields();
        // 遍历属性信息
        for (Field field : fields) {
            // 属性的修饰符
            int modifiers = field.getModifiers();
            String mod = Modifier.toString(modifiers);
            // 属性名称
            String fieldName = field.getName();
            // 属性的数据类型
            Class<?> type = field.getType();
            // 属性数据类型转换为字符串,此时属性类型的字符串时没有包名称的
            String typeName = type.getSimpleName();
            // 打印属性信息
            System.out.println(mod+" "+ typeName +" "+fieldName);
        }
    }

    /**
     * 根据属性名称获取Class对象对应的属性信息。例如:根据ArrayList属性名称size获取对应的Field对象
     * 步骤:
     * 1 调用Class对象的getDeclaredField(name)方法,根据属性名称获取对应的Field对象
     * 2 调用Field对象的成员方法,获取属性的修饰符、数据类型、属性名等信息
     */
    @Test
    public void getFieldByNameTest() {
        Class<ArrayList> clazz = ArrayList.class;
        try {
            // private int java.util.ArrayList.size
            Field sizeField = clazz.getDeclaredField("size");
            System.out.println(sizeField);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.4 获取Class对象的构造方法(Constructor)信息

Constructor提供了单个构造方法的信息,以及对该构造方法进行访问。

获取构造方法信息如下:

public java.util.ArrayList(java.util.Collection)
public java.util.ArrayList()
public java.util.ArrayList(int)
package com.atguigu.reflect6;

import org.junit.Test;

import java.lang.reflect.Constructor;
import java.util.ArrayList;

/**
 * 获取ArrayList.class的构造方法信息(Constructor)
 */
public class ArrayListReflectClass3 {

    /**
     * 获取ArrayList的Class对象所有构造方法信息
     * 步骤:
     * 1 调用Class对象的getDeclaredConstructors()方法获取所有的构造方法信息Constructor
     * 2 遍历每一个Constructor对象
     * 3 打印Constructor对象
     */
    @Test
    public void getAllConstructorTest() {
        Class<ArrayList> clazz = ArrayList.class;
        // 获取ArrayList的Class对象所有的Constructor对象
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }
    }

    /**
     * 根据参数类型获取某一个构造方法信息Constructor
     * 例如:public java.util.ArrayList(int)
     * 步骤:
     *  1 调用Class对象的getDeclaredConstructor方法根据参数类型获取某一个
     *    构造方法信息Constructor
     *  2 打印Constructor对象
     */
    @Test
    public void getConstructorTest() {
        Class<ArrayList> clazz = ArrayList.class;
        try {
            Constructor<ArrayList> construct = clazz.getDeclaredConstructor(int.class);
            // public java.util.ArrayList(int)
            System.out.println(construct);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.5 获取Class对象的方法(Method)信息

Method提供了类或接口上单个方法的信息以及对该方法的访问。

获取方法信息如下:

public void add(int arg0,Object arg1)
public boolean remove(Object arg0)
public Object remove(int arg0)
public Object get(int arg0)    
public Object set(int arg0,Object arg1)
package com.atguigu.reflect6;

import org.junit.Test;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;

public class ArrayListReflectClass4 {

    /**
     * 获取ArrayList所有的方法信息(Method)
     * 步骤:
     *  1 调用Class的getDeclaredMethods()方法获取ArrayList所有的方法信息
     *  2 逐个遍历每个Method方法信息
     *  2.1获取方法的修饰符
     *  2.2获取方法的返回类型
     *  2.3获取方法的名称
     *  2.4获取方法的参数列表
     */
    @Test
    public void getAllMethodsTest() {
        Class<ArrayList> clazz = ArrayList.class;
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            // --获取方法的修饰符
            int modifiers = method.getModifiers();
            String mod = Modifier.toString(modifiers);
            // --获取方法的返回类型
            Class<?> returnType = method.getReturnType();
            String typeName = returnType.getSimpleName();
            // --获取方法的名称
            String methodName = method.getName();
            // --获取方法参数列表信息
            Parameter[] params = method.getParameters();
            // 参数列表有多个,使用StringBuilder拼接参数列表
            StringBuilder sb = new StringBuilder();
            // 遍历参数列表
            for (Parameter param : params) {
                // 参数类型
                Class<?> paramType = param.getType();
                String paramTypeName = paramType.getSimpleName();
                // 参数名称
                String paramName = param.getName();
                sb.append(",").append(paramTypeName).append(" ").append(paramName);
            }
            // 去掉第一个,(逗号)
            String paramsInfo = sb.toString();
            paramsInfo = paramsInfo.replaceFirst(",","");
            // 打印方法修饰符、方法返回类型、方法名称、参数列表
            System.out.println(mod+" "+typeName+" "+methodName+"("+paramsInfo+")");
        }
    }

    /**
     * 根据方法名称获取某一个Method对象信息
     * 步骤:
     *  1 调用Class对象的getDeclaredMethod(name,Class<?>...clazz)方法获取方法信息并返回Method对象
     *  2 打印Method对象
     */
    @Test
    public void getMethodByName() {
        Class<ArrayList> clazz = ArrayList.class;
        try {
            Method method =clazz.getDeclaredMethod("remove",int.class);
            System.out.println(method);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.6 使用反射创建对象

两种方式:

1、直接通过Class对象来实例化(要求必须有无参构造)

2、通过获取构造器对象来进行实例化

方式一的步骤:

(1)获取该类型的Class对象(2)创建对象

package com.atguigu.reflect7;

import org.junit.Test;

/**
 * 使用反射创建对象
 */
public class StudentReflectTest1 {

    /**
     * 使用反射创建对象
     * 步骤:
     *  1. 通过反射入口获取Student的Class对象
     *  2. 调用Class对象的newInstance()方法创建Student对象
     *  3. 打印创建的Student对象
     */
    @Test
    public void createStudentTest(){
        Class<Student> clazz = Student.class;
        try {
            Student student = clazz.newInstance();
            System.out.println(student);
        }  catch (Exception e) {
            e.printStackTrace();
        }
    }
}

方式二的步骤:

(1)获取该类型的Class对象(2)获取构造器对象(3)创建对象

示例代码:

package com.atguigu.reflect7;

import org.junit.Test;

import java.lang.reflect.Constructor;

/**
 * 通过获取构造器对象来进行实例化Student对象
 */
public class StudentReflectTest2 {

    /**
     * 使用构造器对象来创建Student带有参数的对象
     * 例如:public Student(String stuName, int stuScore)
     * 1 反射入口,获取Student的Class对象
     * 2 调用Class的getDeclaredConstructor(Class<?>...clazz)方法获取构造器对象Constructor
     * 3 调用Constructor的newInstance(Object...obj)方法创建Student对象
     * 4 打印创建的Student对象
     */
    @Test
    public void createStudentTest(){
        Class<Student> clazz = Student.class;
        try {
            /*
               获取构造器对象
             * 参数1:String类型的Class对象
             * 参数2:基本数据类型int的Class对象
             */
            Constructor<Student> structor =
                    clazz.getDeclaredConstructor(String.class, int.class);
            /*
            	创建实例对象
                参数1:String类型实际的参数值
                参数2:int类型实际的参数值
                注意:参数类型对象和参数值必须匹配
             */
            Student tom = structor.newInstance("Tom", 18);
            System.out.println(tom);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.7 使用反射调用成员方法

前提:必须要创建对象才能调用方法,因为成员方法是跟着对象的

案例:使用反射创建Student对象,调用对象的成员方法setName

案例:使用反射创建Student对象,调用对象的成员方法getName

示例代码:

package com.atguigu.reflect7;

import org.junit.Test;

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

public class StudentReflectTest3 {
    /**
     * 案例:使用反射创建Student对象,调用成员方法setName
     * 步骤:
     *  1.反射入口:获取Student对应的Class对象
     *  2.调用Class对象的getDeclaredConstructor()方法获取构造器对象Constructor
     *  3.调用构造器对象Constructor的newInstance()方法创建对象
     *  4.调用Class对象的getDeclaredMethod(name,args)方法根据方法名称获取对应的Method对象
     *  5.调用Method对象的invoke()方法,绑定对象名称和参数列表,完成方法的调用
     */
    @Test
    public void invokeMethodTest() {
        Class<Student> clazz = Student.class;
        try {
            Constructor<Student> structor = clazz.getDeclaredConstructor();
            Student stu = structor.newInstance();
            System.out.println("setStuName方法调用之前:"+stu);
            // 根据方法名称和参数列表定位到唯一的方法对象Method
            Method method = clazz.getDeclaredMethod("setStuName", String.class);
            // 方法调用
            // 以前是:对象名.setStuName(值);
            // 现在是:方法对象.invoke(学生对象,值)
            method.invoke(stu, "ABCDEFG");
            System.out.println("setStuName方法调用之后"+stu);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 使用反射创建Student对象,调用getStuName方法,并打印方法的返回结果
     * 步骤:
     *  1. 反射入口:获取Student对应的Class对象
     *  2. 调用Class对象的getDeclaredConstructor()方法获取构造器对象Constructor
     *  3. 调用构造器对象Constructor的newInstance()方法创建对象
     *  4. 调用Class对象的getDeclaredMethod()方法根据方法名称获取对应的Method对象
     *  5. 调用Method对象的invoke()方法调用getStuName方法
     *  6. 打印调用结果
     */
    @Test
    public void invokeMethodTest2() {
        Class<Student> clazz = Student.class;
        try {
            // 一个类有多个构造方法,通过参数列表定位到唯一的一个构造方法
            Constructor<Student> structor =
                    clazz.getDeclaredConstructor(String.class,int.class);
            Student stu = structor.newInstance("Jerry",90);
            Method method = clazz.getDeclaredMethod("getStuName");
            // 调用方法
            // 以前是:对象名.getStuName();
            // 现在是:方法对象.invoke(学生对象);
            Object result = method.invoke(stu);
            System.out.println("方法调用结果 = "+result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.8 暴力破解属性

暴力破解:使用反射调用对象的私有数据

场景:使用暴力破解来访问Student的stuName私有属性,然后为其赋值

关键步骤:

  1. 设置属性可访问性,true表示可以访问私有属性

field.setAccessible(true);

  1. 设置属性值,参数1:属性的对象,参数2:属性值

field.set(obj,“Robert”);

  1. 获取属性值
    Object value = field.get(obj);
package com.atguigu.reflect7;

import org.junit.Test;

import java.lang.reflect.Field;

public class StudentReflectTest4 {

    /**
     * 场景:使用暴力破解来访问Student的stuName私有属性,然后为其赋值
     * 步骤:
     *  1. 反射入口:获取Student对应的Class对象
     *  2. 调用Class对象的newInstance()方法创建对象
     *  3. 调用Class对象的getDeclaredField("属性名")方法根据属性名获取对应的属性对象Field
     *  4. 调用Field对象的setAccessible(true)方法,设置可访问对象的私有属性
     *  5. 调用Field对象的set(对象名,属性值)方法,为私有属性赋值
     *  6. 调用Field对象的get(对象名)方法,获取私有属性值
     *  7. 打印属性值
     */
    @Test
    public void accessibleTest() {
        Class<Student> clazz = Student.class;
        try {
            Student stu = clazz.newInstance();
            Field stuNameField = clazz.getDeclaredField("stuName");
            // 设置可以访问对象的私有属性
            stuNameField.setAccessible(true);
            // 为私有属性赋值
            stuNameField.set(stu,"AAAAA");
            // 获取私有属性的值
            Object value = stuNameField.get(stu);
            System.out.println(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.3.9 暴力破解构造器

暴力破解构造器:使用反射调用对象的私有构造器(构造方法)

案例:使用暴力破解访问Student的私有构造方法,然后创建对象

关键步骤:

​ setAccessible(true); 设置可访问私有构造器

package com.atguigu.reflect7;

import org.junit.Test;

import java.lang.reflect.Constructor;

public class StudentReflectTest5 {

    /**
     * 案例:使用暴力破解访问Student的私有构造方法Student(String stuName),然后创建对象
     * 步骤:
     *  1. 反射入口:获取Student对应的Class对象
     *  2. 根据参数类型获取对应的构造器对象Constructor
     *  3. 调用构造器对象Constructor的setAccessible(true)方法设置可访问私有构造器
     *  4. 调用私有构造器创建对象
     *  5. 打印对象信息
     */
    @Test
    public void accessConstructorTest() {
        Class<Student> clazz = Student.class;
        try {
            Constructor<Student> struct =
                    clazz.getDeclaredConstructor(String.class);
            // 设置可访问私有构造器
            struct.setAccessible(true);
            // 调用私有构造器创建对象
            Student student = struct.newInstance("AAABBB");
            System.out.println(student);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值