首页 > 编程语言 >001_【博学谷学习记录】超强总结,用心分享,狂野大数据:Java篇

001_【博学谷学习记录】超强总结,用心分享,狂野大数据:Java篇

时间:2023-01-10 01:33:29浏览次数:64  
标签:Java String int 狂野 System 001 println public out

title: Java
tags:
  - Java语法
  - 数组
  - 面向对象
  - 集合
  - 常用API
  - 文件
  - IO流
  - Socket编程
  - Junit测试
  - 日志
  - 异常
  - 反射
  - 注解
  - 动态代理
  - jdk8特性
  - jdk源码
categories:
  - Java
urlname: java
abbrlink: 20128
date: 2022-07-08 07:27:05

img

语言是工具,是基石,是规则,吾等站在巨人的肩膀上,得以望见远方,当一杯敬过往,一杯敬朝阳,一杯留给后人敬仰。

java基础语法

开发前言

java语言排行榜

image-20220709070150258

Java语言概述

Java是一种高级编程语言,而且是面向对象的编程语言。

Java语言是美国Sun公司(Stanford University Network),在1995年推出的高级的编程语言。

Java语言共同创始人之一:詹姆斯·高斯林 (James Gosling),被称为“Java之父”

Java语言的版本:1.0-1.4,5.0...8.0...11...17

目前我们学习使用的8.0

Java语言能做什么

Java语言主要应用在互联网程序的开发领域

  • 网上购物商城
  • 物流
  • 金融
  • 各行各业的门户网站

Java语言的跨平台实现原理

JVM: Java虚拟机,是专门用来运行Java程序的

平台: 指的就是操作系统,比如windows,linux,macos等

跨平台: 我们编写的一个Java程序,可以做多个操作系统上运行【一次编译,到处运行】

1.问题1

Java程序是跨平台的?

正确的,一次编译到处运行

2.问题2

JVM是跨平台的?

错误的,java的虚拟机本身不具备跨平台功能的。JVM是实现Java程序跨平台的基石,针对不同的操作系统提供不同版本的JVM,而程序在JVM中运行

3.问题3

Java程序的跨平台是依靠JVM的不夸平台实现的

正确的

image-20220727010926066

JDK_JRE_JVM的组成和作用

JVM: Java虚拟机,是专门用来运行Java程序的,但是不能单独安装

JRE: Java运行环境,包含JVM(Java虚拟机,是专门用来运行Java程序的)和核心类库,jre=jvm+运行类库

JDK: Java开发工具包,包含JRE和开发工具,jdk=jre+编译器等开发工具

三者关系: JDK > JRE > JVM

image-20200420095512929

Java语言开发环境搭建

JDK安装

文件扩展名显示隐藏

image-20220523093753992

jdk的下载和安装

1.注意操作系统是windows,linux,MacOS

2.注意操作系统的位数是32位还是64位

3.安装java相关软件的时候: 安装路径中不允许出现中文和空格

常用DOS命令的使用

如何进入DOS命令操作窗口?

1.开始/命令提示符

2.开始/搜索程序和文件 输入cmd

3.windows键 + R --> 输入cmd

4.窗口空白处/按住shift键 + 鼠标右键单击/在此处开命令窗口

image-20200330102544966

image-20200330102611049

环境变量JAVA_HOME的配置

记事本软件的启动方式?

通过记事本的启动了解系统寻找软件的路径

  • 1.开始/程序/附件/记事本
  • 2.C:/windows/找到notepad.exe命令,双击启动
  • 3.如果在DOS窗口的命令中:
    • C:\windows> notepad.exe+回车 运行这个命令
    • 首先在C:\windows路径下,寻找是否存在notepad.exe,发现有,直接运行
    • D:\abc> notepad.exe+回车 运行这个命令
    • 首先:在D:\abc路径下,寻找是否存在notepad.exe,发现没有
    • 其次: 如果发现在当前路径D:\abc没有要运行的notepad.exe命令,到系统环境变量path中寻找
    • path:... C:\Windows;...,发现path中配置的路径C:\Windows有该命令,直接运行.如果path中配置的所有的路径中都没有要运行的命令,运行报错了.

给Java配置环境变量的意义/目的/作用?

让我们可以在任意路径下运行java开发的相关工具(javac: 编译工具,java: 运行工具)

比如jdk的安装路径:C:\develop\Java\jdk1.8.0_162

配置步骤:

  • 1.创建名称为JAVA_HOME的环境变量,取值是 C:\develop\Java\jdk1.8.0_162

  • 2.把步骤1中创建的名称为JAVA_HOME的环境变量,添加到系统环境变量path中,找到系统环境变量path,在前面添加: %JAVA_HOME%\bin;...

    • 为什么java_home不直接带上bin?
      • 因为java 不只用到 /bin 下的目录,很多程序还需要用到 java_home 目录下的其它东西
  • 3.如果在DOS窗口的命令中:

    • C:\develop\Java\jdk1.8.0_162\bin> javac.exe+回车 运行这个命令

    • 首先在C:\develop\Java\jdk1.8.0_162\bin路径下,寻找是否存在javac.exe,发现有,直接运行

    • D:\abc> javac.exe+回车 运行这个命令
      首先:在D:\abc路径下,寻找是否存在javac.exe,发现没有

    • 其次: 如果发现在当前路径D:\abc没有要运行的javac.exe命令,到系统环境变量path中寻找

    • path:... %JAVA_HOME%\bin;...,发现path中配置的名称为JAVA_HOME的环境变量,对应的路径C:\develop\Java\jdk1.8.0_162\bin中有要运行的javac.exe命令,直接运行,如果path中配置的所有路径中,都没有要运行的javac.exe命令,运行报错了

    • 寻找名称为JAVA_HOME的环境变量,找到后,使用其配置的具体路径进行替换:

    • path:... C:\develop\Java\jdk1.8.0_162\bin;...,

    • 替换后的路径: C:\develop\Java\jdk1.8.0_162\bin中有javac命令,就可以直接运行

image-20220727011825209

Q: 变量名一定要叫JAVA_HOME吗?

A: 不一定,但是建议叫JAVA_HOME,后续要使用的软件,只会识别该名称

比如tomcat要识别这个变量用到jdk的类库

image-20220727011722421

java文件区分大小写,为什么dos执行javac编译成功?

windows不区分大小写,所以编译可以

找不到或无法加载主类?

classpath不要配

HelloWorld入门程序

程序开发的步骤

1.源程序:

  • 程序员写的程序
  • 程序员在自己可以看得懂得程序
  • 程序: 字母,数字,其他符号
  • 源程序是程序员编写的,程序员自己可以看得懂得程序,本质就是一个文本文件,但是扩展名不是.txt,而是.java

2.生产JVM可以执行的字节码(.class)文件

  • JVM: 叫做Java虚拟机,是专门用来运行Java程序的
  • 但是JVM是一个二货,只能识别0和1,而存储0和1的文件叫做字节码文件(.class文件)
  • 如何把源文件(程序)翻译成JVM能够执行的字节码文件(程序)呢?
  • 使用javac命令(编译命令)
  • 使用格式: javac 文件名.java
  • 编译HelloWorld.java源文件: javac HelloWorld.java
  • 生成一个字节码文件: HelloWorld.class

3.把字节码文件交给JVM执行

  • 不管是源文件(程序)还是字节码文件(程序)都存储在硬盘中?
  • 不会自动执行,如何把字节码文件交给JVM执行呢?
    • 使用java命令(运行命令),使用格式: java 文件名,java HelloWorld

HelloWorld案例的编写编译运行

1.编写源文件
    创建一个名称为HelloWorld.txt的文本文件,把扩展名修改为.java
    打开HelloWorld.java源文件,输入以下内容,并保存(ctrl+s)
    public class HelloWorld {
        public static void main(String[] args){
            System.out.println("HelloWorld");
        }
    }

2.编译: javac命令
    根据.java源文件生产对应的.class文件(字节码文件)
    使用javac命令的格式:
		javac 文件名.java
        javac HelloWorld.java
            
    注意:
		(1)保证当前路径下javac命令可以使用
        (2)保证当前路径下有要进行编译的源(.java)文件
        (3)使用编译javac命令时,文件名后面必须写扩展名.java
            
3.运行: java命令            
    把字节码(.class)文件交给jvm执行
    使用java命令的格式:
		java 文件名
        java HelloWorld 
    注意:
		(1)保证当前路径下java命令可以使用
        (2)保证当前路径下有要进行运行的字节码(.class)文件
        (3)使用运行java命令时,文件名后面不能写扩展名.class

HelloWorld案例的常见问题

  • 非法字符问题。Java中的符号都是英文格式的。
  • 大小写问题。Java语言对大小写敏感(区分大小写)。
  • 在系统中显示文件的扩展名,避免出现HelloWorld.java.txt文件。
  • 编译命令后的java文件名需要带文件后缀.java
  • 运行命令后的class文件名(类名)不带文件后缀.class
  • 不要把main写成mian

注释和关键字和保留字和补码

注释

1.概念:
	用来解释说明程序的文字,是给程序员看的,不会影响程序的编译和运行效率。
2.分类:
    (1)当行注释: //  只能写一行内容
	(2)多行注释: /* 可以写多行(1行,2行...)内容 */
	(3)文档注释: /** 可以写多行(1行,2行...)内容  */
        
        
//这里是定义一个类,类的名字叫做Demo01ZhuShi,
//而且文件名必须和类的名字保持一模一样,public class 目前是固定写法,目前记住,后面讲解
public class Demo01ZhuShi {
	/*
		这里是定义main方法,public static void main(String[] args)是固定写法
		main方法是程序的入口
	*/
	public static void main(String[] args){
		/*
			这是一个输出语句,用来向控制台输出显示内容的,
			()中的""里面的内容会被输出显示到控制台上
		*/
		System.out.println("zhushi....");
	}
}

关键字

/*
	邮箱:	@前面是用户名 @后面是使用的是哪家的邮箱
	
		[email protected]			正确的
		[email protected]				正确的
		[email protected]	正确的
		zhangsan@[email protected]	错误的

		@理解为在邮箱当中具有特殊的含义和使用方式,不能胡乱用,看做邮箱名称中的关键字
	
	关键字:
		1.概念: 是指在程序中,Java已经定义好的单词,具有特殊含义和特殊使用方式。
			具体的哪些单词是关键字,它们的特殊含义和用法是什么?
			要求能够辨识出程序中的关键字
			
		2.特点:
			(1)所有的字母都是小写的
			(2)高级编辑器中彩色显示
			
		3.说出以下程序中的关键字:
			public class static void 
*/
public class Demo02GuanJianZi {
	public static void main(String[] args){
		System.out.println("guanjianzi....");
	}
}

保留字

日后能够会提升为关键字,即不能用作标识符。如,goto,const.

源码反码补码

所有数据的运算都是采用补码进行的

  • 正数(符号位/最高位为0)原反补相同
  • 负数(符号位为1)
已知原码求补码: 0b10110100
1 1001011(反码)
0  0000001(+1)
---------------
1  1001100(补码)
已知补码求原码: 0b11101110
1   1101110(补码)
0   0000001(-1)
----------------
1   1101101(反码)
1   0010010(原码)

常量

常量的概念和分类

数学中有常数的概念:
	y = x + 5;	//数字5是一个常数,其值不可以发生改变  
	b = a + 5.5;	//数字5.5是一个常数,其值不可以发生改变

数学中对常数进行了分类:
	比如: 
		数字5是一个整数常数,其值不可以发生改变
		数字5.5是一个小数数常数,其值不可以发生改变
            
数学中的常数,对应到java中叫常量,数学中的常数有分类,java中的常量也有分类,而且比数学中的分类更加丰富

1.概念:	在程序的执行过程中,其值不可以发生改变的量
2.分类:
	(1)整数常量:	100		200
    (2)小数常量:	5.5		7.7
    (3)字符常量:
		java中规定字符常量必须使用单引号''引起来,而且单引号''中只能写一个字符(不能不写,也不能写2个以上)
        举例:
			A:		'a'							正确的
            B:		''		里面什么都没有写  	   错误的(编译器会报错)
            C:		' '		里面有一个空格			正确的
            D:		'ab'						错误的				
            E:		'好'						    正确的
            F:		'女子'					   错误的
     (4)布尔常量:只有两个值true和false
     		true: 表示肯定的,对的,是的,正确的,成立的
            false:表示否定的,错的,不是的,却无的,不成立的
     (5)字符串常量:
		java中规定字符串常量必须使用双引号""引起来,而且双引号""中可以写多个字符(0个,1个,2个....)
        举例:
			A:		"a"								正确的
            B:	    "" 			里面什么都没有写	  正确的
            C:  	" "			里面有一个空格		   正确的
            D:		"ab"							正确的
            E:		"好"							   正确的
            F:		"女子"						   正确的
                
     (6)空常量: null
                
		快捷键: ctrl + d 复制一行
	 
	 	System.out.println(xxx);//把xxx输出到控制台,并换行
	 	System.out.print(xxx);//把xxx输出到控制台,不换行

打印不同类型的常量

public class Demo03ChangLiang {
	public static void main(String[] args){
		//(1)整数常量:	100		200		
		System.out.println(100);		
		System.out.println(200);
		
		//(2)小数常量:	5.5		7.7
		System.out.println(5.5);
		System.out.println(7.7);
		
		//(3)字符常量:
		System.out.println('a');
		//System.out.println('');//错误的: ''不能没有字符
		System.out.println(' ');//正确的: ' '有一个空格
		System.out.println('好');//正确的
		//System.out.println('女子');//错误的: '女子'不能写2个及以上的字符
		
		//(4)字符串常量
		System.out.println("a");
		System.out.println("");
		System.out.println(" ");
		System.out.println("ab");
		System.out.println("好想你");
		System.out.println("女子");
		
		//(5)布尔常量
		System.out.println(true);
		System.out.println(false);
		
		//(6)空常量
		//System.out.println(null);//错误: 不能直接打印空常量null
        
        char[] chars={'a','b','c'};
        System.out.println(chars); // abc
        int[] ints={1,2,3};
        System.out.println(ints);// [I@282ba1e
        //char[]可以直接打印内容,因为println底层实现了
        //而int[]只能打印出数组地址,因为底层没有实现
        //ctrl+p可以查看print方法接收参数的详情
	}
}
static final double PI = 3.14;
常量不是PI,而3.14,PI是标识符,是变量。常量是值。不要理解错了。
这里final修饰的变量为常量,指的是变量PI指向常量的值不能改变了。故称为常量,不要混淆了。
备注:是因为基本数据类型,所以值不会变了,如果是引用类型,只是引用指向的地址值不变,内容可以变。

进制与字节

进制及转换

进制

进制的概念:逢几进一就叫做几进制
进制的分类:
    十进制:	逢十进一		每位的数字0-9
    二进制:	逢二进一		每位的数字0-1
    八进制:	逢八进一		每位的数字0-7
    十六进制:  逢十六进一	   每位的数字0-9,10(A/a),11(B/b),12(C/c),13(D/d),14(E/e),15(F/f)
        

转换

1.十进制转十进制
    (十进制数字的每一位(从右向做)上隐藏了一个10的多少次方,第1位是10的0次方,第2位是10的1次方...):
    十进制数字1234(x^y: x的y次方):       
		1234 = 4 + 30 + 200 + 1000
             = 4*10^0 + 3*10^1 + 2*10^2 + 1*10^3
            
2.二进制转十进制(8421编码):	(系数*基数的权次幂)
	二进制数字10010:
		10010 = 1 * 2 ^ 4 + 0 * 2 ^ 3 + 0 * 2 ^ 2 + 1 * 2 ^ 1 + 0 * 2 ^ 0 = 18
	(二进制数字的每一位(从右向做)上隐藏了一个2的多少次方,第1位是2的0次方,第2位是2的1次方...):
    1101 = 1*2^0 + 0*2^1 + 1*2^2 + 1*2^3
         = 1*1 + 0*2 + 1*4 + 1*8
       	 = 1 + 0 + 4 + 8
         = 13
      
    1111 = 1*2^0 + 1*2^1 + 1*2^2 + 1*2^3
       	 = 1*1 + 1*2 + 1*4 + 1*8
         = 1 + 2 + 4 + 8
         = 15
        
  
3.十进制转二进制: 除以2取余数,倒过来写(除积倒取余)
    十进制的13转换成二进制:	1101
	十进制的75转换成二进制:	1001011

十进制转二进制图解

十进制转2进制:除以2取余数,倒过来写

image-20220727012734608

计算机中的存储单位(2的10次方就是1024)

1.位(bit): 计算机中存储一个数字0或者1所占用的空间   简写成b
2.字节(Byte): 8个bit(比特,二进制位)0000-0000表示为1个byte(字节,1B),简写成B
    字节是我们常见的计算机中最小存储单元。
    1B = 8b
    1024B = 1KB
    1024KB = 1MB
    1024MB = 1GB
    1024GB = 1TB
    ....
    
    务必记住:
		1个字节是8位

变量和数据类型

变量概念及分类

数学中有个常数的概念:
y = x + 10; 	//整数数字10是不可以发生变化的
b = a + 6.6;	//小数数字6.6是不可以发生变化的

数学中的数字(常量)是有分类的,对应java中的常量也是有分类的
    
x,y是可以发生变化的
	x: 2 y: 12
	x: 6 y: 16    

x,y中的数据是可以发生变化的,而且x,y内部的数据也是有类型(整数)
    
      
a,b是可以发生变化的
  a: 2.2 b: 8.8
  a: 3.3 b: 9.9    
b,b中的数据是可以发生变化的,而且a,b内部的数据也是有类型(小数)
      
像x,y,a,b 这样东西,里面的数据是可以发生变化的,而且数据是有类型的,我们把这样的东西称为变量(容器: 里面只能放一个数据)      

变量为什么要有这么多的分类: 不同的分类,占用的字节数不同,取值范围就不同,使用的场景也就不同

1.变量概念: 在程序的执行过程中,其值可以在一定范围内发生改变的量
2.分类:
	(1)整数
        byte		1个字节		-128到127
        short		2个字节		正负3万多
        int			4个字节		正负21亿		整数默认int类型
        long		8个字节		大概19位数字	   表示long类型数据后面需要加L/l
        
    (2)小数
        float		4个字节		表示float数据后面需要加F/f
        	注意: 虽然float占4个字节,但是由于采用科学计数法,取值范围远远超过long
        double		8个字节		小数默认double类型
                
    (3)字符:
		char		2个字节		0-65535
    (4)布尔:
		boolean		1个字节		取值为true或者false
            
// 两个变量互换值
temp = x; 
x = y ; 
y = temp;

a = a + b; 
b = a - b; 
a = a - b;

a = a ^ b; 
b = a ^ b; 
a = a ^ b;

注意点:

  • 变量在使用之前必须要赋值
  • 留意float和long类型变量定义要加后缀,float加F,long加L
  • 因为整数默认int,想表示一个数为long要加L,
  • 因为浮点型默认double,想表示一个数为float要加F
  • 引用数据类型(类,数组,接口,字符串,lambda,枚举enum,注解)
  • 基本数据类型(4类8种:整数,浮点数,字符,布尔): byte,short,int,long; float,double; char; boolean;
    • 浮点型是近似值而非精确值,4个字节的float要比8个字节的long大。
    • boolean为一个字节理论上只需要1/8字节(一位bit就可以决定true/false),但是java中没有明确指定大小。
    • int a = 1; byte b = 2;int j = b + a; // b自动补充3个字节后计算
    • byte,short,char运算时自动提升为int
    • 引用数据类型==比较的是地址值
    • 字符串new的在堆中,直接双引号写的在堆中的常量池里

变量定义格式图解分析

变量的理解:
1.变量的本质就是内存中的一块空间,空间的大小由数据类型决定
2.要想找到变量对应的内存空间的数据,需要给变量对应的内存空间起个名字,叫做变量名称
3.变量对应的内存空间中必须有数据才能使用,这种向变量内存空间中,存储数据的过程叫做初始化或者赋值

变量的定义格式一(先挖坑,然后种萝卜):
	数据类型 变量名称;//先挖坑
	变量名称 = 数据值;//再种萝卜
	
变量的定义格式二(挖坑,同时种萝卜):
	数据类型 变量名称 = 数据值;//挖坑,同时种萝卜
	
变量的定义格式三(先挖多个坑,然后分别向每个坑中种萝卜):
	数据类型 变量名称1,变量名称2,变量名称3;//先挖多个坑
	变量名称1 = 数据值1;//向第一个坑中种萝卜
	变量名称2 = 数据值2;//向第二个坑中种萝卜
	变量名称3 = 数据值3;//向第三个坑中种萝卜
	
变量的定义格式四(挖多个坑,同时分别向每个坑中种萝卜):
	数据类型 变量名称1 = 数据值1,变量名称2 = 数据值2,变量名称3 =数据值3 ;	
	

图解:

内存可以理解为田地,变量理解为萝卜坑
1.一个萝卜一个坑 
2.大萝卜放大坑 
3.小萝卜放小坑
Java 程序中是役有萝卜的,有的数据 
1.整数的萝卜放整数的坑中 
2.小数的萝卜放小数的坑中

程序在JVM(内存)中运行

image-20220727013205917

定义8种变量

public class Demo01BianLiang {
	public static void main(String[] args){
		/*
			变量的定义格式一(先挖坑,然后种萝卜):
			数据类型 变量名称;//先挖坑
			变量名称 = 数据值;//再种萝卜
		*/
		byte a;//挖了一个byte类型(1个字节)的坑,给这个坑起个名字叫a
		
		a = 66;//把数字66存储到byte类型的坑a中
		
		System.out.println(a);//打印byte类型(1个字节)的坑a中的内容: 66
		
		a = 88;//把数字88存储到byte类型的坑a中,原有的数据66将被替换
		
		System.out.println(a);//打印byte类型(1个字节)的坑a中的内容: 88
		
		/*
			变量的定义格式二(挖坑,同时种萝卜):
				数据类型 变量名称 = 数据值;//挖坑,同时种萝卜
		*/
		short b = 100;//挖了一个short类型(2个字节)的坑,给这个坑起个名字叫b,同时向这个坑中存储数字100
		
		System.out.println(b);//打印short类型(2个字节)的坑b中的内容: 100
		
		/*
			变量的定义格式三(先挖多个坑,然后分别向每个坑中种萝卜):
			数据类型 变量名称1,变量名称2,变量名称3;//先挖多个坑
			变量名称1 = 数据值1;//向第一个坑中种萝卜
			变量名称2 = 数据值2;//向第二个坑中种萝卜
			变量名称3 = 数据值3;//向第三个坑中种萝卜
		*/
		int c,d,e;//挖了三个int类型(4个字节)的坑,给每个坑分别起名为c,d,e
		
		c = 200;//把数字200存储到int类型的坑c中
		d = 300;//把数字300存储到int类型的坑d中
		e = 500;//把数字500存储到int类型的坑e中
		
		System.out.println(c);//打印int类型(4个字节)的坑c中的内容: 200
		System.out.println(d);//打印int类型(4个字节)的坑d中的内容: 300
		System.out.println(e);//打印int类型(4个字节)的坑e中的内容: 500
		
		/*
			变量的定义格式四(挖多个坑,同时分别向每个坑中种萝卜):
			数据类型 变量名称1 = 数据值1,变量名称2 = 数据值2,变量名称3 =数据值3 ;
		*/
		//挖了三个long类型的坑,名字分别叫做f,g,h
		//同时把600L存储到坑f中
		//同时把700L存储到坑g中
		//同时把800L存储到坑h中
		long f = 600L,g = 700L,h = 800L;
		
		System.out.println(f);//打印long类型(8个字节)的坑f中的内容: 600
		System.out.println(g);//打印long类型(8个字节)的坑g中的内容: 700
		System.out.println(h);//打印long类型(8个字节)的坑h中的内容: 800
		
	}
}
public class Demo02BianLiang {
	public static void main(String[] args){
		//float类型
		//定义float类型变量a,并初始化
		//大萝卜不能直接存储到小坑中
		//float a = 6.6;//错误: 6.6默认是double类型,占8个字节,不能存储到4个字节的float变量中
		
		float a = 6.6F;
		System.out.println(a);//打印变量a中的内容
		
		//double类型
		//定义double类型变量b,并初始化
		double b = 8.8;
		System.out.println(b);//打印变量b中的内容
		
		//char类型
		//定义char类型变量c1,并初始化
		char c1 = 'a';
		System.out.println(c1);//打印变量c1中的内容
		
		//char c2 = '';//错误: ''中不能不写字符
		//System.out.println(c2);//打印变量c2中的内容
		
		//char c3 = 'ab';//错误: ''中不能写2个及以上的字符
		//System.out.println(c3);//打印变量c3中的内容
		
		//boolean类型: 只能存储true或者false
		//定义boolean类型变量d1,并初始化
		boolean d1 = true;
		System.out.println(d1);//打印变量d1中的内容
		
		d1 = false;//把false存储到变量d1中,原有的数据将被替换
		
		System.out.println(d1);//打印变量d1中的内容
		
		//d1 = 100;//错误: 数据类型不匹配
		//System.out.println(d1);//打印变量d1中的内容
		
		
	}
}

变量的注意事项

变量定义的注意事项:
	1.变量名称:在同一个大括号范围内,变量的名字不可以相同。
	2.变量赋值:定义的变量,不赋值不能使用。
	3.定义long类型的变量时,需要在整数的后面加L(大小写均可,建议大写)。
		因为整数默认是int类型,整数太大可能超出int范围。
		
	4.定义float类型的变量时,需要在小数的后面加F(大小写均可,建议大写)。
		因为浮点数的默认类型是double, double的取值范围是大于float的,类型不兼容。
        
public class Demo03BianLiangNotice {
	public static void main(String[] args){
		
		//定义int变量a,并初始化
		int a = 100;		
		System.out.println(a);
		
		//错误: 不能在同一个区域({}),定义同名的变量
		//int a = 200;
		//System.out.println(a);
		
		//定义int变量b,未赋值
		int b;
		//System.out.println(b);//错误: b中没有值,不能使用
		
		b = 200;//把数字200赋值给变量b
		System.out.println(b);
		
		//long c = 6000000000;//错误: 6000000000(60亿)默认是int类型,但是大小已经远远超过int的取值范围(正负21亿)了
		//System.out.println(c);
		
		long d = 6000000000L;//6000000000L: 是long类型的数据
		System.out.println(d);
		
		//错误: 大萝卜不能直接放入小坑中
		//float e = 6.6;//错误: 6.6默认是double类型,占8个字节,不能赋值给4个字节的float变量e
		//System.out.println(e);
		
		float f = 6.6F;//6.6F: 是float类型的数据
		System.out.println(f);
	}
}

标识符的含义及注意事项

标识符:
	1.概念: 程序中起名字的地方(类名,方法名称,变量名)
	2.命名规则: 硬 性 要 求
		标识符可以包含 英文字母26个(区分大小写) 、 0-9数字 、 $(美元符号) 和 _(下划线) 。
		标识符不能以数字开头。
		标识符不能是关键字。
	3.命名规范: 软 性 建 议
        类名规范:首字母大写,后面每个单词首字母大写(大驼峰式)。
			Demo01BianLiang
			Demo02BianLiang
			Demo03BianLiangNotice
			Demo04BiaoShiFu
				
		方法名规范: 首字母小写,后面每个单词首字母大写(小驼峰式)。
			getMin(...){...}
			getMax(...){...}
				
		变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。
			num
			value
			maxValue
public class Demo04BiaoShiFu {
	public static void main(String[] args){
		int b2;//正确
		//int b*2;//错误: 不能包含*
		
		//int 2b;//错误: 不能以数字开头
		
		//int public;//错误: 不能是关键字。
		
		//按照小驼峰规则,定义变量
		int ageOfMyGirlFriend = 18;
		System.out.println(ageOfMyGirlFriend);
	}
}

数据类型转换

自动类型转换【从小到大自动】

Java程序中要求参与的计算的数据,必须要保证数据类型的一致性,如果数据类型不一致将发生类型的转换。
    int + int
    int + long ==> long + long  (把int转换成long: 从小到大,自动类型转换,不需要代码的干预)
    int + long ==> int + int  (把long转成int: 从大到小,强制类型转换,必须手动代码完成)
    

1.自动类型转换概念:
	取值范围小的数据或者变量可以直接赋值给取值范围大的变量(小萝卜可以直接放入大坑中)
        
2.特点: 
	(1)自动类型转换是自动完成的,不需要代码的干预
    (2)byte/short/char类型数据,只要参加运算会自动转换为int类型    
    (3)byte、short、char-->int-->long-->float-->double
举例:
有一个byte类型(1个字节)的数字5:			00000101	
byte类型自动类型转换成short类型(2个字节):
	在左侧补充1个字节的0,因为左侧补充的都是0,对原有数据是没有影响的,仍然是5
	00000000 00000101 

byte类型自动类型转换成int类型(4个字节): 
	在左侧补充3个字节的0,因为左侧补充的都是0,对原有数据是没有影响的,仍然是5
	00000000 00000000 00000000 00000101
    
byte类型自动类型转换成long类型(8个字节):  
	在左侧补充7个字节的0,因为左侧补充的都是0,对原有数据是没有影响的,仍然是5
	00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

总结:
	根据需求,在数据前面补充若干字节的0,因为补充的都是0,对原有数据大小是没有影响的(打肿脸充胖子)
public class Demo05Convert {
	public static void main(String[] args){
		int i = 1; 
		byte b = 2; 
		/*
			b是byte类型,i是int类型,运算时类型不一致,会发生自动类型转换
			byte类型(1个字节)的b会自动转换成int类型(4个字节):在byte类型的b左侧补充3个字节的0
			
			最终变成了两个int数据相加,结果是int类型(占用4个字节),不能直接赋值给左侧的byte类型的变量x,占用1个字节
			
			大萝卜不能直接放入小坑中
		*/
		//byte x = b + i; 
		//System.out.println(x);
		
		/*
			b是byte类型,i是int类型,运算时类型不一致,会发生自动类型转换
			byte类型(1个字节)的b会自动转换成int类型(4个字节):在byte类型的b左侧补充3个字节的0
			
			最终变成了两个int数据相加,结果是int类型(占用4个字节),可以直接赋值给左侧的int类型的变量y,占用4个字节
			
			大萝卜可以直接放入大坑中
		*/
		int y = b + i;
		System.out.println(y);//3
	}
}
public class Demo06Convert {
	public static void main(String[] args){
		int i = 1; 
		double d = 2.5;
		/*
			i是int类型,d是double类型,运算时类型不一致,会发生自动类型转换
			int类型(4个字节)的i会自动转换成double类型(8个字节): 最终效果就是在整数后面添加.0 比如: 1变成1.0
			
			最终变成了两个double数据相加,结果是double类型(占用8个字节),不能直接赋值给左侧的int类型的变量x,占用4个字节
			
			大萝卜不能直接放入小坑中
		*/
		//int x = i + d;		
		//System.out.println(x);
		
		/*
			i是int类型,d是double类型,运算时类型不一致,会发生自动类型转换
			int类型(4个字节)的i会自动转换成double类型(8个字节): 最终效果就是在整数后面添加.0 比如: 1变成1.0
			
			最终变成了两个double数据相加,结果是double类型(占用8个字节),可以直接赋值给左侧的double类型的变量y,占用8个字节
			
			大萝卜可以直接放入大坑中
		*/
		double y = i + d;
		System.out.println(y);
	}
}

强制类型转换【从大到小强制】

1.强制类型转换概念:	
	取值范围大的数据或者变量不能直接赋值给取值范围小的变量(大萝卜不能直接放入小坑中)
        解决方案:
			(1)把坑变大
            (2)把萝卜变小(强制类型转换)

2.格式:
	转后类型 变量名称 = (转后类型)转前数据或者变量;
	long类型(8个字节)的数字5:
	long num = 5L;
	long类型强制类型转换成int类型(4个字节):
	int a = (int)num;//把num中的数据强制类型转换成int类型,并把结果赋值给int变量a

举例:
有一个long类型(8个字节)的数字5:	
	00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

long类型强制类型转换成int类型(4个字节): 
	砍掉左侧的四个字节的内容,因为砍掉的都是数字0,所以对最终的结果数据没有影响仍然是5
    00000000 00000000 00000000 00000101    
	

long类型强制类型转换成short类型(2个字节):
	砍掉左侧的六个字节的内容,因为砍掉的都是数字0,所以对最终的结果数据没有影响仍然是5
	00000000 00000101
        
long类型强制类型转换成byte类型(1个字节):
	砍掉左侧的七个字节的内容,因为砍掉的都是数字0,所以对最终的结果数据没有影响仍然是5
    00000101    

总结: 
    根据需求,砍掉数据左侧的若干字节的数据,只要砍掉的都是0,对原数据没有影响
        但是只要砍掉的数据中包含1,就会对原数据产生影响(可能会损失精度)

精度损失:

public class Demo07Convert {
	public static void main(String[] args){
		double d = 1.5;
		/*
			左侧d是double类型(占8个字节),右侧的int类型的变量x(占4个字节)
			相当于: 左侧是较大的数据,右侧的变量比较小
			大萝卜不能直接放入小坑中
		*/
		//int x = d;
		//System.out.println(x);
		/*
			左侧d是double类型(占8个字节),右侧的int类型的变量x(占4个字节)
			double数据是不能直接赋值给int变量的: 大萝卜不能直接放入小坑中
			但是进行了强制类型转换,把double数据强制转换成int数据
			
			double强制类型转换成int: 直接把小数部分干掉,会损失精度
		*/
		
		int y = (int)d;
		
		System.out.println(y);
	}
}

数据溢出:

public class Demo08Convert {
	public static void main(String[] args){
		short s = 1;
		/*
			s是short类型,1是int类型,运算时类型不一致,会发生自动类型转换
			short类型(2个字节)的s会自动转换成int类型(4个字节):在short类型的s左侧补充2个字节的0
			
			最终变成了两个int数据相加,结果是int类型(占用4个字节),不能直接赋值给左侧的short类型的变量s,占用2个字节
			
			大萝卜不能直接放入小坑中
		*/
		//s = s + 1;		
		//System.out.println(s);
		
				
		/*
			s是short类型,1是int类型,运算时类型不一致,会发生自动类型转换
			short类型(2个字节)的s会自动转换成int类型(4个字节):在short类型的s左侧补充2个字节的0
			
			最终变成了两个int数据相加,结果是int类型(占用4个字节),不能直接赋值给左侧的short类型的变量s,占用2个字节
			
			但是在赋值之前把int类型的结果,强制转换成short类型(砍掉左侧的2个字节的内容),
			由于砍掉的2个字节都是0,所以最终的结果没有影响,仍然是2
			
			把萝卜变小
		*/
		s = (short)(s + 1);
		System.out.println(s);
	}
}

图解(其它案例):

image-20200508120946828

ASCII码表

计算机是一个二货,只能存储0和1,所以存储到计算机中的所有内容都会转换成0和1进行存储
所以我们在计算机中存储的字符也不例外,也需要把字符转换成0和1进行存储
问题: 如何把字符转换成0和1呢?
通过ASCII编码表: 存储字符和数字对应关系的一张表格
    
存储字符时:需要查找ASC码表,找到字符对应的数字,将数字转换为二进制数存放到计算机中
	'A'	---> 65 ---> 1000001		大写字母是连续的,ASCII编码值依次+1
    'a' ---> 97 ---> 1100001		小写字母是连续的,ASCII编码值依次+1
    '0' ---> 48 ---> 110000		    数字字符是连续的,ASCII编码值依次+1
    
使用字符时:将对应的二进制数转换为十进制 找到ASC表中对应的字符 
    1000001 ---> 65 ---> 'A'
    1100001 ---> 97 ---> 'a'
    110000  ---> 48 ---> '0'

//字符类型和int类型计算
char c = 'a';
int i = 1;
System.out.println(c+i);//输出结果是98
#  char类型的字符先查询编码表,得到97,再和1求和
    
//关于大小写转换
char c1 = 'a' - 32;
System.out.println("c1 = " + c1); // A
char c2 = 'A' + 32;
System.out.println("c2 = " + c2); // a
// 'a'先查编码表,得到97,再和32计算,赋值给char1
// 常量优化机制:int型也可以赋值给byte,short,char
// 超出数据类型范围时打印结果不对。

image-20200508115505469

ASCII编码:美国用的,但是别国的语言不够,各国各自为政,(大陆:GB2312,GBK;台湾:BIG5;欧洲:ISO8859--1);后大一统,Unicode,通常两个字节,但是,全英文的不划算,因此,可变长度编码UTF-8,英文一个字节,中文3个,偏僻汉字4-6个字节

int类型和char类型的运算原理

public class Demo09Char {
	public static void main(String[] args){
		//定义char类型的变量ch,并初始化为'A'
		char ch = 'A';
		System.out.println(ch);//A
		/*
			自动类型转换中:
				byte/short/char类型,只要参加运算,会自动转换为int类型
				
			ch是char类型,在参加运算时,自动转为int类型
			问题:
				char类型如何转换为int类型的数字呢?
				查看ASCII编码表: 找到'A'对应的数字是65,然后参与运算
				
		*/
		System.out.println(ch+1);//66
		/*
			自动类型转换中:
				byte/short/char类型,只要参加运算,会自动转换为int类型
				
			ch是char类型,在参加运算时,自动转为int类型
				查看ASCII编码表: 找到'A'对应的数字是65,然后参与运算
				所以: ch + 1 的结果是66
				
			然后把int数字66强制转换成char类型的数据
			问题:
				int类型的数据如何强制转换成char类型数据呢?
				查看ASCII编码表: 找到int数字66对应的字符'B'显示出来
		*/
		System.out.println((char)(ch+1));//B
	}
}
byte a = 3;
byte b = 4;
byte c = a + b; // 报错=> byte c = (byte)(a+b)  // 会自动提升为int计算,所以要强制转换一下。
byte d = 3 + 4; //不会报错 常量优化机制,会先计算,判断在byte范围内

运算符

算术运算符加减乘除

+ - * / % ++(自增) --(自减)

1.运算符:对常量或者变量进行操作的符号 
2.表达式:用运算符把常量或者变量连接起来符合java语法的式子就可以称为表达式。
3.数学运算符:
	(1)+: 加法运算
    (2)-: 减法运算
    (3)*: 乘法运算
    (4)/: 除法运算
/*
	算数运算符
		1.运算符:对常量或者变量进行操作的符号 
		2.表达式:用运算符把常量或者变量连接起来符合java语法的式子就可以称为表达式。
		3.数学运算符:
			(1)+: 加法运算 (String 相加底层会创建StringBuilder)
			(2)-: 减法运算
			(3)*: 乘法运算
			(4)/: 除法运算
				被除数 ÷ 除数 = 商(/: 取的就是商) ......	余数
*/
public class Demo10Operator {
	public static void main(String[] args){
		int a = 3;
		int b = 2;
		System.out.println(a + b);//3 + 2: 5 
		System.out.println(a - b);//3 - 2: 1 
		System.out.println(a * b);//3 * 2: 6 
		//3/2: int/int 结果必然是int类型
		System.out.println(a / b);//3 / 2: 1 
		
		//int * double / int ==> double /	int ==> double / double ==> double
		System.out.println((a*1.0) / b);//3.0 / 2 ==> 3.0/2.0 ==> 1.5 				 
	}
}

算术运算符%

/*
	%运算符: 取余数(模)运算符
		被除数 ÷ 除数 = 商(/: 取的就是商) ......	余数(%: 取的就是余数)
		
	作用:
		1.判断数字的奇偶性: 
			数字%2 结果是0 说明数字是偶数
			数字%2 结果不是0 说明数字是奇数
			
		2.判断一个数字是否能够被另外一个数字整除
			结果为0: 说明可以整除
			结果不为0: 说明不可以整除
			
		3.可以把%和/结合使用计算数字的个位,十位,百位,千位
			比如有个int变量num,保存数字1234
			int num = 1234;
			个位: num%10
			十位: num/10%10
			百位: num/100%10
			千位: num/1000%10
*/
public class Demo11Operator {
	public static void main(String[] args){
		System.out.println(10%2);//0 说明10是偶数
		System.out.println(11%2);//1 说明11是奇数
		
		System.out.println(100%25);//0 说明100可以被25整除
		System.out.println(100%26);//22 说明100不可以被26整除
		
		System.out.println("---------------");
		int num = 1234;
		System.out.println(num%10);//4: 个位    1234 ÷ 10 = 商123 .... 余数4(%)
		//System.out.println(num/10);//123	    1234 ÷ 10 = 商123(/) .... 余数4(%)
		System.out.println(num/10%10);//3 十位  123 ÷ 10 = 商12(/) ...  余数3(%)
		
		//System.out.println(num/100);//12     1234 ÷ 100 = 商12(/) ... 余数34(%)
		
		System.out.println(num/100%10);//2 百位 12 ÷ 10 = 商1(/) ...  余数2(%)
		
		System.out.println(num/1000%10);//1 千位
        
        // 负数的取模运算结果是不是负数看左边
        System.out.println(5 % -2); // 1
        System.out.println(-2 % 5); // -2
	}
}

image-20200421144143935

算术运算符+的特殊用法

/*
	+符号的作用
		1.数学中的加法运算(数字相加,字符相加)
		2.字符串的拼接(把两个字符串连在一起)
*/
public class Demo12Operator {
	public static void main(String[] args){
		System.out.println(5+5);//10
		/*
			int + char ==> int + int ==> int
			需要:
				char ==> int 查看ASCII码表 'A'对应65
		*/
		System.out.println(5+'A');//5 + 65: 70
		/*
			自动类型转换中:
				byte/short/char类型,只要参加运算,会自动转换为int类型
			char + char ==> int + int ==> int
			需要:
				char ==> int 查看ASCII码表 'A'对应65
				char ==> int 查看ASCII码表 'B'对应66
		*/
		System.out.println('A'+'B');//65 + 66: 131
		
		System.out.println("Hello"+"World");
		//"5+5="+5+5: 从左向右计算
		//先计算"5+5="+5: 此处+号代表字符串的连接 结果是"5+5=5"
		//然后"5+5=5"+5: 此处+号代表字符串的连接 结果是"5+5=55"
		System.out.println("5+5="+5+5);//5+5=55
		
		//()的优先级是比较高的,所以先计算5+5 结果10
		//然后"5+5="+10: 此处+号代表字符串的连接 结果是"5+5=10"
		System.out.println("5+5="+(5+5));//5+5=10

		
	}
}

赋值运算符

= += -= *= /= %= 自动类型强转功能

/*
	基本赋值运算符: =
	复合赋值运算符:
		+=		a+=b		a=a+b
		-=		a-=b		a=a-b
		*=		a*=b		a=a*b
		/=		a/=b		a=a/b
		%=		a%=b		a=a%b
*/
public class Demo13Operator {
	public static void main(String[] args){
		int a = 10,b = 20;
		a += b;//a = a + b
		System.out.println(a);//30
		System.out.println(b);//20
		
		int c = 30,d = 20;
		c %= d;//c = c % d = 30%20 = 10
		
		System.out.println(c);//10
		System.out.println(d);//20
        
        // 赋值运算符与变量类型转换(自动强转)
        byte b1=1;
        //b1= b1+ 256;//编译报错
        b1+=256;
        System.out.println(b1);//值为1,而不是257.int 强转为 byte ,溢出
        int i1=10;
        i1*=1.234;
        System.out.println(i1);//值为12,而不是12.34,double强转为int精度损失
	}
}

赋值运算符的特点

/*
	赋值运算符特点
		1.+=,-=,/=,*=,%= 运算结果的数据类型和左侧变量的数据类型不一致,隐藏强制类型转换
		2.整数常量只要不超出所赋值的整数变量的取值范围,可以直接赋值,内部隐藏强制类型转换
*/
public class Demo14Operator {
	public static void main(String[] args){
		short s = 1;
		/*
			s是short类型,1是int类型,运算时类型不一致,会发生自动类型转换
			short类型(2个字节)的s会自动转换成int类型(4个字节):在short类型的s左侧补充2个字节的0
			
			最终变成了两个int数据相加,结果是int类型(占用4个字节),不能直接赋值给左侧的short类型的变量s,占用2个字节
			
			大萝卜不能直接放入小坑中
		*/
		//s = s + 1;		
		//System.out.println(s);
		
				
		/*
			s是short类型,1是int类型,运算时类型不一致,会发生自动类型转换
			short类型(2个字节)的s会自动转换成int类型(4个字节):在short类型的s左侧补充2个字节的0
			
			最终变成了两个int数据相加,结果是int类型(占用4个字节),不能直接赋值给左侧的short类型的变量s,占用2个字节
			
			但是在赋值之前把int类型的结果,强制转换成short类型(砍掉左侧的2个字节的内容),
			由于砍掉的2个字节都是0,所以最终的结果没有影响,仍然是2
			
			把萝卜变小
		*/
		s = (short)(s + 1);
		System.out.println(s);
		
		short s2 = 1;
		/*
			+=,-=,/=,*=,%= 运算结果的数据类型和左侧变量的数据类型不一致,隐藏强制类型转换
		*/
		s2 += 1;//s2 = (short)(s2 + 1);
		
		System.out.println(s2);
		/*
			右侧10是int类型(4个字节),左侧变量byte类型(1个字节)
			按照道理来讲: 大萝卜是不可以直接放入小坑中的
			现在为什么可以呢?
			原因是数字10是一个常量,值并没有超出byte的取值范围
			所以可以直接赋值,内部会帮助我们进行强制类型转换
			等价于:
				byte b = (byte)10;
		*/
		byte b = /*(byte)*/10;
		System.out.println(b);//10
	}
}

自增自减运算符

/*
	自增(++)自减(--)运算符(代码演示只演示++)
		1.作用: 让变量的值增加1(++)或者减少1(--)
		2.使用格式:
			(1)可以写在变量的前面: ++a,--a
			(2)可以写在变量的后面: a++,a--
		
		3.使用特点:
			(1)单独使用: ++/--自己独占一行,没有其它运算一起参与
				前++和后++,没有任何区别,都是让变量的值增加1
				前--和后--,没有任何区别,都是让变量的值减少1
				
			(2)混合使用: 和其它运算(赋值,打印等)一起
				前++/--: 先++/--,后再使用		先给变量的值增加(++)或者减少(--)1,然后再使用++/--后的结果
				后++/--: 先使用,然后++/--		先使用变量的值,再把变量的值	增加(++)或者减少(--)1
		
		
	重点:					----最常用的东西
		a++: 变量a的值增加1
		a--: 变量a的值减少1
*/
public class Demo15Operator {
	public static void main(String[] args){
		int a = 2;
		//++自己独占一行,没有其它运算一起参与
		a++;//a = a + 1 = 2 + 1
		System.out.println(a);//3
		
		int b = 2;
		//++自己独占一行,没有其它运算一起参与
		++b;//b = b + 1 = 2 + 1
		System.out.println(b);//3
		
		System.out.println("-----------------");
		int c = 2;
		/*
			目前是++和赋值一起操作,属于混合运算
			而且++写在了c的前面,先把c的值增加1,
			c的值变为3,然后再把结果3赋值给变量d,d的值3			
		*/
		int d = ++c;
		
		System.out.println(c);//3
		System.out.println(d);//3
		System.out.println("-----------------");
		
		int e = 2;
		/*
			目前是++和赋值一起操作,属于混合运算
			而且++写在了e的后面,所以先使用e的值(2)赋值给变量f,所以f的值是2,
			然后e的值再增加1,变成3
		*/
		int f = e++;
		
		System.out.println(e);//3
		System.out.println(f);//2
		
		System.out.println("-----------------");
		
		int x = 4; //5 6
		/*
			表达式(x++)+(++x)+(x*10)是从左到右计算的
			先计算(x++): 因为++在后面,先使用x的值4,然后x的值增加,变成5
			4 + (++x)+(x*10)
			接着计算(++x): 因为++在前面,先把x的值增加1,x变成6,然后再使用6
			4 + 6+(x*10)
			接着计算x*10 -->  6*10 结果: 60
			4 + 6 + 10 结果: 70
		*/
		int y = (x++)+(++x)+(x*10);
				// 4 + 6 + 
		System.out.println(x);//6
		System.out.println(y);//70
		
	}
}

关系运算符

== != < > <= >= 结果为布尔值true/false

1.作用:
	用来比较数据之间的大小关系
        
2.特点:
	不管关系表达式多么复杂或者多么简单,返回值一定是布尔类型的结果,要么是true,要么是false
    布尔值也可以比较,System.out.println(true == false); // false
        
3.分类:
	== 		a==b,判断a和b的值是否相等,成立为true,不成立为false
    != 		a!=b,判断a和b的值是否不相等,成立为true,不成立为false
    > 		a>b,判断a是否大于b,成立为true,不成立为false
    >= 		a>=b,判断a是否大于或者等于b,成立为true,不成立为false
    < 		a<b,判断a是否小于b,成立为true,不成立为false
    <= 		a<=b,判断a是否小于或者等于b,成立为true,不成立为false
        
4.注意:
	(1)=: 一个等号是赋值的意思
    (2)==: 两个等号是判断是否相同的意思
public class Demo01GuanXi {
    public static void main(String[] args) {
        int a = 10, b = 20;
        boolean result = (a == b);
        System.out.println(result);//false
        System.out.println(a != b);//10 != 20: true
        System.out.println(a > b);//10 > 20: false
        System.out.println(a >= b);//10 >= 20: false
        System.out.println(a < b);//10 < 20: true
        System.out.println(a <= b);//10 <= 20: true
        System.out.println(a == b);//10 == 20: false
        System.out.println(a = b);//20 把变量b的值赋值给变量a,最后打印变量a的值
    }
}

逻辑运算符

& &&(短路与) | ||(短路或) ^ !

举例: 咱们班哥们在黑马学习java,顺利毕业,高薪就业,找到了对象,谈了好长时间,需要谈婚论嫁
到你对象家和准岳母谈判:
	准岳母:
		小伙子呀,你必须得有房子(条件1: true/false),然后呢,你必须还得有车子(条件2: true/false)		
        以上的要求: 两个条件都得满足(true),这个事才能确定下来			使用逻辑运算符	&
            
        小伙子呀,你要么有房子(条件1: true/false),你呀要么有车子(条件2: true/false)		
        以上的要求: 两个条件只要有一个满足(true),这个事就能确定下来			使用逻辑运算符	|
            
    
1.作用: 
	用来连接多个条件(布尔表达式的: 结果为true/false的式子),最终的结果也必须是一个布尔类型的数据,要么是true,
	要么是false
    不管逻辑运算符连接的式子有多么简单或者多么复杂,最终结果要么是true,要么是false    
2.分类:
	(1)&(shift+7): 逻辑与,表示并且的意思,多个条件同时成立的意思,就是只有多个条件都是true,最终的结果才是true
        特点:
			【有false,则false】: 只要有一个条件不成立(false),结果就是false
    (2)|(shift+\): 逻辑或,表示或者的意思,多个条件,只要有一个成立,最终的结果就是true
        特点:
        	【有true,则true】:只要有一个条件是true,结果就是true
    (3)^(shift+6): 逻辑异或,相同为false,不同为true			----基本不用
    (4)!(shift+1): 逻辑取反,!true 就是false,!false 就是true
        
public class Demo02LuoJi {
    public static void main(String[] args) {
        int a = 10,b = 20,c = 30;
        System.out.println(a>b & a>c);//10 > 20 & 10 > 30 ==> false & false ==> false
        System.out.println(a<b & a<c);//10 < 20 & 10 < 30 ==> true & true ==> true
        System.out.println(a>b & a<c);//10 > 20 & 10 < 30 ==> false & true ==> false
        System.out.println(a<b & a>c);//10 < 20 & 10 > 30 ==> true & false ==> false

        System.out.println("--------------------");

        System.out.println(a>b | a>c);//10 > 20 | 10 > 30 ==> false | false ==> false
        System.out.println(a<b | a<c);//10 < 20 | 10 < 30 ==> true | true ==> true
        System.out.println(a>b | a<c);//10 > 20 | 10 < 30 ==> false | true ==> true
        System.out.println(a<b | a>c);//10 < 20 | 10 > 30 ==> true | false ==> true

        System.out.println("--------------------");
        System.out.println(a>b ^ a>c);//10 > 20 ^ 10 > 30 ==> false ^ false ==> false
        System.out.println(a<b ^ a<c);//10 < 20 ^ 10 < 30 ==> true ^ true ==> false
        System.out.println(a>b ^ a<c);//10 > 20 ^ 10 < 30 ==> false ^ true ==> true
        System.out.println(a<b ^ a>c);//10 < 20 ^ 10 > 30 ==> true ^ false ==> true
        System.out.println("--------------------");
        System.out.println(!true);//false
        System.out.println(!false);//true
    }
}

逻辑运算符的短路效果

逻辑运算符的短路效果
	1.短路的逻辑运算符
    	(1)短路逻辑与&&: 左侧为false,右边不计算
        (2)短路逻辑或||: 左侧为true,右侧不计算
    2.特点:
        (1)短路逻辑与&&: 和&结果是相同的,但是&&可以提高效率
        (2)短路逻辑与||: 和|结果是相同的,但是||可以提高效率

    3.建议:
		短路表示一个逻辑表达式的所有部分不必都执行下去,提升的性能很可观
        以后开发学习中,全部使用短路与&& 以及 短路或||
public class Demo03DuanLu {
    public static void main(String[] args) {
        int a = 2;
        /*
            整个表达式(3>5)&&(++a>2)从左向右计算
            先计算表达式3>5结果为false
            因为两个表达式使用&&连接,左侧为false,已经决定了最终的结果为false,
            不管右侧表达式(++a>2)的结果是true还是false,都无法改变&&的最终结果,
            所以右侧表达式(++a>2)不进行计算
         */
        System.out.println((3>5)&&(++a>2));//false
        System.out.println(a);//2: 说明++a没有计算,&&右侧的表达式没有执行

        int b = 2;
        /*
            整个表达式(3>5)&(++b>2)从左向右计算
            先计算表达式3>5结果为false
            因为两个表达式使用&连接,左侧为false,虽然已经决定了最终的结果为false,
            但是右侧表达式(++b>2)仍然要进行计算,所以b的值最终是3
         */
        System.out.println((3>5)&(++b>2));//false
        System.out.println(b);//3: 说明++b进行计算,&右侧的表达式执行了

        System.out.println("-------------------");
        int c = 2;
        /*
            整个表达式(3<5)||(++c>2)从左向右计算
            先计算表达式3<5结果为true
            因为两个表达式使用||连接,左侧为true,已经决定了最终的结果为true,
            不管右侧表达式(++c>2)的结果是true还是false,都无法改变||的最终结果,
            所以右侧表达式(++c>2)不进行计算
         */
        System.out.println((3<5)||(++c>2));//true
        System.out.println(c);//2: 说明++c没有计算,||右侧的表达式没有执行

        int d = 2;
        /*
            整个表达式(3<5)|(++d>2)从左向右计算
            先计算表达式3<5结果为true
            因为两个表达式使用|连接,左侧为true,虽然已经决定了最终的结果为true,
            但是右侧表达式(++d>2)仍然要进行计算,所以d的值最终是3
         */
        System.out.println((3<5)|(++d>2));//true
        System.out.println(d);//3: 说明++d进行计算,|右侧的表达式执行了
    }
}

三元运算符格式

1.格式:
	数据类型 变量名称 = 布尔表达式1 ? 表达式2 : 表达式3;

2.执行流程:
	(1)计算布尔表达式1的结果,看是true还是false
    (2)如果布尔表达式1的结果为true,就把表达式2的结果赋值给左侧的变量
    (3)如果布尔表达式1的结果为false,就把表达式3的结果赋值给左侧的变量

3.建议:
    高效简洁编程,但是要考虑可读性。
	数据类型 变量名 = 布尔类型表达式 ? 真值 : 假值;
	真值假值的数据类型要和变量接收的数据类型匹配(一致或满足自动转换)
    编译的时候就要类型统一

执行流程图解:

image-20200423115658540

三元运算符的练习之两只老虎(相等)

需求:
    动物园里有两只老虎,已知两只老虎的体重分别为180kg、200kg,
    请用程序实现判断两只老虎的体重是否相同。

实现步骤:
	1.定义两个int变量w1和w2,分别代表两只老虎的体重,并按照题目要求进行初始化
	2.使用三元运算符判断w1和w2的值是否相同,保存到boolean变量result中
    3.打印result的值
        
public class Demo01SanYuan {
    public static void main(String[] args) {
        //1.定义两个int变量w1和w2,分别代表两只老虎的体重,并按照题目要求进行初始化
        int w1 = 180, w2 = 200;

        //2.使用三元运算符判断w1和w2的值是否相同,保存到boolean变量result中

        boolean result = (w1 == w2) ? true : false;

        //3.打印result的值
        System.out.println("两只老虎的体重相同吗? "+result);

        System.out.println("---------------");

        String s = (w1 == w2) ? "相同" : "不相同";

        System.out.println("两只老虎的体重相同吗? "+s);
    }
}

三元运算符的练习之两只老虎(最大值)

需求:
    动物园里有两只老虎,已知两只老虎的体重分别为180kg、200kg,
    请用程序实现计算两只老虎的体重的最大值。

实现步骤:
	1.定义两个int变量w1和w2,分别代表两只老虎的体重,并按照题目要求进行初始化
    2.使用三元运算符,计算w1和w2的最大值,把结果保存到int变量max中
    3.打印max的值
public class Demo02SanYuanMax {
    public static void main(String[] args) {
        //1.定义两个int变量w1和w2,分别代表两只老虎的体重,并按照题目要求进行初始化
        int w1 = 180, w2 = 200;

        //2.使用三元运算符,计算w1和w2的最大值,把结果保存到int变量max中

        int max = (w2 > w1) ? w2 : w1;

        //3.打印max的值
        System.out.println("两只老虎体重的最大值: "+max);
    }
}

图解分析

image-20220728091011215

三元运算符的练习之三个和尚

需求:
    一座寺庙里住着三个和尚,已知他们的身高分别为150cm、210cm、165cm,
    请用程序实现获取这三个和尚的最高身高。

实现步骤:
	1.定义3个int变量h1,h2,h3代表三个和尚的身高,并根据题目需求进行初始化
    2.使用三元运算符计算出h1和h2的最大值,保存到int变量temp中
    3.使用三元运算符计算出temp和h3的最大值,保存到int变量max中
    4.最终打印max的值
        
public class Demo03SanYuanMax {
    public static void main(String[] args) {
        //1.定义3个int变量h1,h2,h3代表三个和尚的身高,并根据题目需求进行初始化
        int h1 = 150, h2 = 210, h3 = 165;

        //2.使用三元运算符计算出h1和h2的最大值,保存到int变量temp中
         int temp = (h1 > h2) ? h1 : h2;

        //3.使用三元运算符计算出temp和h3的最大值,保存到int变量max中
        int max = (temp > h3) ? temp : h3;

        //4.最终打印max的值
        System.out.println("三个和尚的最大身高: "+max);//210
    }
}

图解分析

image-20220727013542102

位运算符

<<(左移) >>(右移) >>>(无符号右移) &(与) |(或) ^(异或) ~(按位取反)

a^a^a = a  // 异或两次,本身不变。

6<<1 // 12 ≈ 6*2^1
6>>1 // 3 偶数:≈ 6/2
7>>1 // 3 奇数:≈ (7-1)/2

数据输入

键盘录入的基本使用

数据输出: 把程序运行的结果数据输出到控制台,从而显示到屏幕上
数据输入: 获取用户从键盘上录入的数据到程序当中,然后程序根据业务需求,对获取到的数据进行相关处理

思考1:
	像数据输入这样的操作使用非常频繁,而且实现起来比较复杂,jdk的开发人员已经把此功能实现好了
    jdk中把获取键盘录入数据的功能放到了java.util包的Scanner类中

思考2: 如何使用键盘录入Scanner类呢?
    java中的数据类型: 基本数据类型(四类八种)和引用数据类型(类,接口,数组,枚举...)
    对于引用类型的使用有固定的使用步骤,而键盘录入Scanner类就是一个引用类型,使用也有固定的步骤

键盘录入Scanner的使用,有固定的3个步骤:                    ----固定格式,先记住
	1.导包(找到我们要使用的东西,告诉jvm我们使用的东西在哪里)
		格式:
        	import 包名.类名;
            import java.util.Scanner;

	2.创建对象:
    	格式:
        	类名 对象名 = new 类名(...);
            类名: 就是之前写代码时class关键字后面的名字
            Scanner sc = new Scanner(System.in);
            注意: System.in是固定写法,后面就业班专门讲解

	3.使用:
		sc.nextInt(): 获取键盘录入的整数数字
	4.练习:
		获取键盘录入的一个int数字并输出到控制台
            (1)导包: import java.util.Scanner;
            (2)创建对象: Scanner sc = new Scanner(System.in);
            (3)获取键盘录入的数字: sc.nextInt()

import java.util.Scanner;                
public class Demo01Scanner {
    public static void main(String[] args) {
        //(1)导包: import java.util.Scanner;
        //(2)创建对象: Scanner sc = new Scanner(System.in);
        Scanner sc = new Scanner(System.in);

        //(3)获取键盘录入的数字: sc.nextInt()
        System.out.println("哥们,请您输入一个整数数字: ");
        int num = sc.nextInt();
        System.out.println("您输入的整数数字: "+num);
    }
}

键盘录入的理解

image-20200423144716089

键盘录入的练习

需求:
    获取键盘录入的两个整数(int类型)数字,在控制台输出求和的结果。
    
实现步骤:
	1.导包: import java.util.Scanner
    2.创建键盘录入Scanner类的对象: Scanner sc = new Scanner(System.in)
    3.获取键盘录入的两个整数数字,分别保存到int变量a和b中
    4.计算a和b的和,保存到int变量sum中
    5.打印sum的值	
public class Demo02ScannerSum {
    public static void main(String[] args) {
        //1.导包: import java.util.Scanner
        //2.创建键盘录入Scanner类的对象: Scanner sc = new Scanner(System.in)
        Scanner sc = new Scanner(System.in);

        //3.获取键盘录入的两个整数数字,分别保存到int变量a和b中
        System.out.println("请输入第一个整数数字: ");
        int a = sc.nextInt();

        System.out.println("请输入第二个整数数字: ");
        int b = sc.nextInt();

        System.out.println(a+"...."+b);

        //4.计算a和b的和,保存到int变量sum中
        int sum = a + b;

        //5.打印sum的值
        System.out.println("和: "+sum);
    }
}

键盘录入改写三个和尚案例

需求:
    一座寺庙里住着三个和尚,已知他们的身高分别为150cm、210cm、165cm,身高需要使用键盘录入
    请用程序实现获取这三个和尚的最高身高。
    
实现步骤:
	1.创建键盘录入Scanner类的对象(不用单独先自己写导包)
		(1)写出单词Scanner,然后按alt + / 根据提示选择相应的类 会自动导包
        (2)一边写,根据提示直接选择相应的类,回车会自动导包
    2.获取键盘录入的三个整数数字,分别代表三个和尚的身高,分别保存到int变量h1,h2,h3中
    3.使用三元运算符求出h1和h2的最大身高,保存到int变量temp中
    4.使用三元运算符求出temp和h3的最大身高,保存到int变量max中
    5.打印max的值
import java.util.Scanner;        
public class Demo03ScannerMax {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象(不用单独先自己写导包)
        Scanner sc = new Scanner(System.in);
        //2.获取键盘录入的三个整数数字,分别代表三个和尚的身高,分别保存到int变量h1,h2,h3中
        System.out.println("请输入第一个和尚的身高(整数数字): ");
        int h1 = sc.nextInt();

        System.out.println("请输入第二个和尚的身高(整数数字): ");
        int h2 = sc.nextInt();

        System.out.println("请输入第三个和尚的身高(整数数字): ");
        int h3 = sc.nextInt();

        System.out.println(h1 + "..." + h2 + "..." + h3);

        //3.使用三元运算符求出h1和h2的最大身高,保存到int变量temp中
        int temp = (h1 > h2) ? h1 : h2;

        //4.使用三元运算符求出temp和h3的最大身高,保存到int变量max中
        int max = (temp>h3) ? temp : h3;

        //5.打印max的值
        System.out.println("三个和尚的最大身高: "+max);
    }
}

流程控制

流程控制分类

就像任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程之中做出选择和判断.

流程:简单来讲所谓流程就是完成一件事情的多个步骤组合起来就叫做一个流程
注意: 在一个程序执行的过程中,各条语句的执行顺序对程序的结果是有直接影响的。
举例:
    结果:正在学习Java编程语言
    流程:学习编程想法 --> 黑马程序员的咨询老师 --> 安排课程 --> 听Java课程	
    
流程控制语句分类
    1.顺序结构: 按照代码的书写顺序,从上而下依次执行
    2.选择/分支结构
        (1)if语句【重点】
        (2)switch语句
    3.循环结构
        (1)for循环【重点】
        (2)while循环
        (3)do-while循环
    

顺序结构

public class Demo01Sequence {
    public static void main(String[] args) {
        //1.顺序结构: 按照代码的书写顺序,从上而下依次执行
        System.out.println("开始.....");
        System.out.println("今天天气不错,我们上午黑马直播java课....A");
        System.out.println("下午还有答疑老师辅导课....B");
        System.out.println("这的确挺爽的....C");
        System.out.println("结束....");
    }
}

选择结构-if

if语句的第一种格式介绍

英文单词if是: 如果的意思
1.if语句的第一种格式:
	if(布尔表达式){
        语句体;
    }
	其它语句;

2.执行流程:
	(1)使用计算if后面()中布尔表达式的结果,看是true,还是false
    (2)如果if后面()中布尔表达式的结果是true,执行if后面{}中的语句体,接着执行其它语句
    (3)如果if后面()中布尔表达式的结果是false,跳过if后面{}中的语句体,直接执行其它语句
        
3.注意:
	(1)if语句的第一种格式,适用于有一种情况的场景 
    (2)if后面()中表达式不管写的多么简单或者多么复杂,最终的结果一定是布尔类型,要么是true,要么是false
    (3)if后面{}中的语句体,要么执行(布尔表达式结果为true)要么不执行(布尔表达式结果为false)
    (4)if后面{}中的语句体: 一条或者多条语句(每条语句末尾处使用分号结尾)
    (5)if后面{}中的语句体的语句体只有一条语句,此时{}可以省略,但是初学者讲义保留    
      

图解:

image-20220727013708596

if语句的第一种格式练习

练习
	需求1:判断a和b的值是否相等,如果相等,就在控制台输出:a等于b
	需求2:判断a和c的值是否相等,如果相等,就在控制台输出:a等于c
public class Demo02If {
    public static void main(String[] args) {
        //需求1:判断a和b的值是否相等,如果相等,就在控制台输出:a等于b
        int a = 10, b = 20;
        if (a == b) {//10 == 20: false,不执行{}中的语句体
            System.out.println(a + "等于" + b);
        }

        if (a != b) {//10 != 20: true,执行{}中的语句体
            System.out.println(a + "不等于" + b);
        }

        //需求2:判断a和c的值是否相等,如果相等,就在控制台输出:a等于c
        int c = 30;
        if (a == c) {//10 == 30: false,不执行{}中的语句体
            System.out.println(a + "等于" + c);
        }

        if(a != c) {//10 != 30: true,执行{}中的语句体
            System.out.println(a + "不等于" + c);
        }
        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

if语句的第二种格式介绍

英文单词if是: 如果的意思
英文单词else是: 否则的意思
1.if语句的第二种格式:
	if(布尔表达式){
        语句体1;
    } else {
        语句体2;
    }
	其它语句;

2.执行流程:
	(1)使用计算if后面()中布尔表达式的结果,看是true,还是false
    (2)如果if后面()中布尔表达式的结果是true,执行if后面{}中的语句体1,接着执行其它语句
    (3)如果if后面()中布尔表达式的结果是false,执行else后面{}中的语句体2,接着执行其它语句
        
3.注意:
	(1)if语句的第二种格式,适用于有两种情况的场景 
    (2)if后面()中表达式不管写的多么简单或者多么复杂,最终的结果一定是布尔类型,要么是true,要么是false
    (3)语句体1和语句体2,只有一个会被执行
    (4)适用于二选一的场景(是与否的场景)
        

图解:

image-20220727013756992

if语句的第二种格式练习判断大小

需求:
    判断a是否大于b,如果是,在控制台输出:a的值大于b,否则,在控制台输出:a的值不大于b
    
实现步骤:
	1.定义2个int变量a和b,并分别初始化
	2.a和b的大小关系有两种情况,所以使用if-else语句对a和b的值进行判断,并输出不同的结果
        
public class Demo03IfElse {
    public static void main(String[] args) {
        //1.定义2个int变量a和b,并分别初始化
        int a = 10, b = 20;

        //2.a和b的大小关系有两种情况,所以使用if-else语句对a和b的值进行判断,并输出不同的结果
        if (a > b) {//10 > 20: false,执行else后面{}中的语句体
            System.out.println(a + "的值大于" + b);
        } else {
            System.out.println(a + "的值不大于" + b);
        }
        System.out.println("main....end....");//模拟格式中的其它语句
        
        //闰年
        //闰年的判断规则如下:
        //1)若某个年份能被4整除但不能被100整除,则是闰年。
        //2)若某个年份能被400整除,则也是闰年。
        if(year%4==0||year%100!=0&&year%400==0){
        	// ...
        }
    }
}
 

if语句的第二种格式练习判断奇偶数

需求:
    任意给出一个整数,请用程序实现判断该整数是奇数还是偶数,并在控制台输出该整数是奇数还是偶数。
    
实现步骤:
	1.创建键盘录入Scanner类的对象
    (1.导包: import java.util.Scanner; 2.创建对象: Scanner sc = new Scanner(System.in);)
    2.获取键盘录入的整数数字,保存到int变量num中
    3.使用if语句的第二种格式(if-else),判断num中的数字,输出对应的奇偶数情况
        num%2==0: 说明num中的数字是偶数
        num%2!=0: 说明num中的数字是奇数
            
public class Demo04IfElseJiOu {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        //(1.导包: import java.util.Scanner; 2.创建对象: Scanner sc = new Scanner(System.in);)
        Scanner sc = new Scanner(System.in);

        //2.获取键盘录入的整数数字,保存到int变量num中
        System.out.println("请输入一个整数数字: ");
        int num = sc.nextInt();
        //System.out.println("您输入的整数是: "+num);

        //3.使用if语句的第二种格式(if-else),判断num中的数字,输出对应的奇偶数情况
        /*if(num%2==0){
            System.out.println(num+"是一个偶数数字");
        } else {
            System.out.println(num+"是一个奇数数字");
        }*/

        if(num%2!=0){
            System.out.println(num+"是一个奇数数字");
        } else {
            System.out.println(num+"是一个偶数数字");
        }


        System.out.println("main....end....");//模拟格式中的其它语句

    }
}

需求:
    任意给出两个整数,请用程序实现求出两个整数的最大值,并输出到控制台。
    
实现步骤:
	1.创建键盘录入Scanner类的对象
    2.获取两个键盘录入的整数数字,分别保存到2个int变量a和b中
    3.定义int变量max,作用是用来保存两个int数字的最大值
    4.使用if-else对a和b中的值进行大小判断
    	4.1如果: a>b 是成立的,说明a是最大的,把a的值赋值给变量max
        4.2否则: a>b 是不成立的,说明b是最大的,把b的值赋值给变量max
    5.打印max的值
            
import java.util.Scanner;            
public class Demo05IfElseMax {
    public static void main(String[] args) {

        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取两个键盘录入的整数数字,分别保存到2个int变量a和b中
        System.out.println("请输入第一个整数数字: ");
        int a = sc.nextInt();

        System.out.println("请输入第二个整数数字: ");
        int b = sc.nextInt();

        //3.定义int变量max,作用是用来保存两个int数字的最大值
        int max;

        //4.使用if-else对a和b中的值进行大小判断
        if(a>b) {//10 > 20 ==> false
            //4.1如果: a>b 是成立的,说明a是最大的,把a的值赋值给变量max
            max = a;
        } else {
            //4.2否则: a>b 是不成立的,说明b是最大的,把b的值赋值给变量max
            max = b;
        }
        //5.打印max的值
        //System.out.println("数字 "+a+"和"+b+"的最大值是: "+max);
        System.out.println("最大值: "+max);

        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

图解分析:

image-20200424103803810

if语句的第三种格式介绍

英文单词if是: 如果的意思
英文单词else是: 否则的意思
1.if语句的第三种格式:
	if (布尔表达式1) { 
        语句体1; 
    } else if (布尔表达式2) { 
        语句体2; 
    }
	…else if (布尔表达式n) {
        语句体n; 
    } else { 
        语句体n+1; 
    }
	其它语句;

2.执行流程:
	(1)首先计算布尔表达式1的值
	(2)如果值为true就执行语句体1;如果值为false就计算布尔表达式2的值
	(3)如果值为true就执行语句体2;如果值为false就计算布尔表达式3的值
	(4)…
	(5)如果没有任何布尔表达式为true,就执行语句体n+1。	

3.注意:
	(1)if语句的第三种格式,适用于有多种情况(大于等于3)的场景 
    (2)if后面()中表达式不管写的多么简单或者多么复杂,最终的结果一定是布尔类型,要么是true,要么是false
    (3)语句体1到语句体n+1,只有一个会被执行
    (4)适用于多选一的场景
    (5)有if就可以在后面写条件,没有if,不能写条件,不能直接在else后面写条件
    (6)最后一个else后面没有if,是用来兜底的,如果上面所有if后的条件都不成立,直接执行最后一个else中的代码

图解:

image-20220727013819083

if语句的第三种格式练习根据数字输出对应的星期

需求:
	键盘录入一个星期数(1,2,...7),输出对应的星期一,星期二,...星期日
演示效果:
	输入 1 输出 星期一
    输入 2 输出 星期二
    输入 3 输出 星期三 
    输入 4 输出 星期四 
    输入 5 输出 星期五 
    输入 6 输出 星期六 
    输入 7 输出 星期日 
    输入 其它数字 输出 数字有误
        
实现步骤:
	1.创建键盘录入Scanner类的对象
	2.获取键盘录入的整数数字,代表星期数,保存到int变量week中
	3.因为week中的数字有7+1种情况,所以使用if语句的第三种格式进行判断,并输出不同的结果内容

import java.util.Scanner;
public class Demo01IfElseIfElseWeek {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取键盘录入的整数数字,代表星期数,保存到int变量week中
        System.out.println("请输入一个1-7的整数数字(代表星期数):");
        int week = sc.nextInt();

        //3.因为week中的数字有7+1种情况,所以使用if语句的第三种格式进行判断,并输出不同的结果内容
        if(week == 1) {
            System.out.println("星期一");
        } else if(week == 2) {
            System.out.println("星期二");
        } else if(week == 3) {
            System.out.println("星期三");
        } else if(week == 4) {
            System.out.println("星期四");
        } else if(week == 5) {
            System.out.println("星期五");
        } else if(week == 6) {
            System.out.println("星期六");
        } else if(week == 7) {
            System.out.println("星期日");
        } else {//隐藏条件: week>7 || week<1
            System.out.println("您输入的星期数不存在,是火星来的吧,哥屋恩...");
        }

        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

import java.util.Scanner;
public class Demo02IfElseIfElseWeek {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取键盘录入的整数数字,代表星期数,保存到int变量week中
        System.out.println("请输入一个1-7的整数数字(代表星期数):");
        int week = sc.nextInt();

        //3.因为week中的数字有7+1种情况,所以使用if语句的第三种格式进行判断,并输出不同的结果内容
        if(week>7 || week<1) {
            System.out.println("您输入的星期数不存在,是火星来的吧,哥屋恩...");
        } else if(week == 1) { //从这里开始向下,隐藏条件: week>=1 && week<=7
            System.out.println("星期一");
        } else if(week == 2) {
            System.out.println("星期二");
        } else if(week == 3) {
            System.out.println("星期三");
        } else if(week == 4) {
            System.out.println("星期四");
        } else if(week == 5) {
            System.out.println("星期五");
        } else if(week == 6) {
            System.out.println("星期六");
        } else /*if(week == 7)*/ {//隐藏条件: week == 7
            System.out.println("星期日");
        }

        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

if语句的第三种格式练习根据成绩进行奖励

需求:
    小明快要期末考试了,小明爸爸对他说,会根据他不同的考试成绩,送他不同的礼物,
    假如你可以控制小明的得分,请用程序实现小明到底该获得什么样的礼物,并在控制台输出。
    
奖励规则:
	95~100 山地自行车一辆 		包含95和100的	数学中表示方式: [95,100]  不包含95和100: (95,100)
    90~94  游乐场玩一次 		 包含90和94的
    80~89  变形金刚玩具一个     包含80和89的
    80以下  胖揍一顿 			 不包含80分的
        
实现步骤:
	1.创建键盘录入Scanner类的对象
    2.获取一个0-100之间的整数数字(代表小明的考试成绩),保存到int变量score中
    3.因为score中的数字有多种(大于3)情况,所以使用if语句的第三种格式进行判断,并输出不同的结果内容
import java.util.Scanner;
public class Demo03IfElseIfElseScore {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取一个0-100之间的整数数字(代表小明的考试成绩),保存到int变量score中
        System.out.println("请输入一个0-100之间的整数数字(代表小明的考试成绩): ");
        int score = sc.nextInt();

        //3.因为score中的数字有多种(大于3)情况,所以使用if语句的第三种格式进行判断,并输出不同的结果内容
        if(score>=95 && score<=100) {
            System.out.println("奖励山地自行车一辆");
        } else if(score>=90 && score<=94) {
            System.out.println("奖励游乐场玩一次");
        } else if(score>=80 && score<=89) {
            System.out.println("奖励变形金刚玩具一个");
        } else if(score>=0 && score<80){
            System.out.println("奖励胖揍一顿");
        } else /*if(score<0 || score>100)*/{//隐藏条件: score<0 || score>100
            System.out.println("您输入的成绩错误,是火星来的吧,哥屋恩...");
        }

        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

import java.util.Scanner;
public class Demo04IfElseIfElseScore {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取一个0-100之间的整数数字(代表小明的考试成绩),保存到int变量score中
        System.out.println("请输入一个0-100之间的整数数字(代表小明的考试成绩): ");
        int score = sc.nextInt();

        //3.因为score中的数字有多种(大于3)情况,所以使用if语句的第三种格式进行判断,并输出不同的结果内容
        //先排除非法数据
        if(score<0 || score>100) {
            System.out.println("您输入的成绩错误,是火星来的吧,哥屋恩...");
        } else if(score>=95 && score<=100) {//执行到这里及以下: score>=0 && score<=100
            System.out.println("奖励山地自行车一辆");
        } else if(score>=90 && score<=94) {
            System.out.println("奖励游乐场玩一次");
        } else if(score>=80 && score<=89) {
            System.out.println("奖励变形金刚玩具一个");
        } else /*if(score>=0 && score<80)*/{//隐藏条件: score>=0 && score<80)
            System.out.println("奖励胖揍一顿");
        }

        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

图解:

image-20200424121125368

注意点:

  • 1.满足条件进行某种处理,if;if…else;if…else if;if…else if…else;
  • 2.if为输出语句的时候就不能转为三元运算符实现。
  • 3.实际开发中if else嵌套如果超过3层,停下来,思考一下有没有更好的方式
  • 4.return可以结束if else的分支判断,并且,if else判断语句外面之后的代码都不执行。

Image

选择结构-switch

switch语句格式和介绍

1.switch语句的格式:
	switch(表达式) {
    	case 常量值1;
            语句体1;
            break;
        case 常量值2;
            语句体2;
            break;
        ...
        case 常量值n;
            语句体n;
            break;
        default:
            语句体n+1;
            break;
    }
	其它语句;

2.执行流程:
	首先计算出表达式的值
	其次,和case依次比较,一旦有对应的值,就会执行相应的语句,在执行的过程中,遇到break就会结束。
	最后,如果所有的case都和表达式的值不匹配,就会执行default语句体部分,然后程序结束掉。

3.注意事项:
	(1)break的作用是用来结束switch语句的,一旦执行break,直接跳出到switch外面的其它语句继续执行
	(2)switch后面()中的表达式的数据类型,只能是以下几种类型:
		基本类型: byte/short/char/int 都可以   -----------------重要,选择题经常考到---------------
        引用类型: String或者枚举
	(3)case 后面只能写常量,而且常量值不能重复,(条件表达式两边必须都为布尔值,int没法和布尔值比较)
	(4)最后一个default的作用:
		用来兜底的,如果所有的case后面的常量值和switch中表达式的值都不相同,就执行default中的内容
	(5)如果default放在最后的话: 后面的break可以省略
	(6)如果所有的case和default后面都有break,那么default和case的顺序可以任意排列,不影响最终的结果
public class Demo01Switch {
    public static void main(String[] args) {
        int choose = 2;
        switch (choose) {//30

            case 1:
                System.out.println("你好~~~~~");
                break;

            case 2:
                System.out.println("我好~~~~~");
                break;

            case 3:
                System.out.println("大家好,才是真的好~~~~~");
                break;

            default:
                System.out.println("他好,我也好~~~~");
                break;
        }
        System.out.println("main....end....");//模拟格式中的其它语句
    }
}
        

执行流程

image-20220728091135297

switch练习根据月份输出对应的季节

需求:
    一年有12个月,分属于春夏秋冬4个季节,
    键盘录入一个月份,请用程序实现判断该月份属于哪个季节,并输出。

演示效果
	输入: 1、2、12 输出:冬季
	输入: 3、4、5 输出:春季
	输入: 6、7、8 输出:夏季
	输入: 9、10、11 输出:秋季
	输入: 其它数字 输出:数字有误

实现步骤(本案例使用switch):
	1.创建键盘录入Scanner类的对象
	2.获取键盘录入的一个1-12的整数数字(代表月份),保存到int变量month中
    3.因为month中的数字有12+1中情况,使用switch语句对month中的值,进行判断,并输出不同的结果
public class Demo02SwitchMonth {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取键盘录入的一个1-12的整数数字(代表月份),保存到int变量month中
        System.out.println("请输入一个1-12的整数数字(代表月份): ");
        int month = sc.nextInt();

        //3.因为month中的数字有12+1中情况,使用switch语句对month中的值,进行判断,并输出不同的结果
        switch (month) {//15
            case 1:
                System.out.println("冬季");
                break;
            case 2:
                System.out.println("冬季");
                break;
            case 12:
                System.out.println("冬季");
                break;
            case 3:
                System.out.println("春季");
                break;
            case 4:
                System.out.println("春季");
                break;
            case 5:
                System.out.println("春季");
                break;
            case 6:
                System.out.println("夏季");
                break;
            case 7:
                System.out.println("夏季");
                break;
            case 8:
                System.out.println("夏季");
                break;
            case 9:
                System.out.println("秋季");
                break;
            case 10:
                System.out.println("秋季");
                break;
            case 11:
                System.out.println("秋季");
                break;
            default:
                System.out.println("您输入的月份不存在,哪个星球来的,哥屋恩...");
                break;
        }
        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

使用case穿透优化根据月份输出对应的季节的案例

使用case穿透优化根据月份输出对应的季节的案例
	发现问题:
    	前面Demo02SwitchMonth.java文件中出现了大量的重复的代码
        1,2,12代码重复,3,4,5代码重复,6,7,8代码重复,9,10,11代码重复
        每三个case中的代码都是相同的

解决方案使用case穿透:
	如果多个连续的case中具有相同的代码和break,可以只保留最后一个case中的代码和break,
    前面的多个case中省略掉代码和break(只保留case)

    在switch语句中,如果case的后面不写break,将出现穿透现象,
    也就是不会在判断下一个case的值,直接向后运行,直到遇到break,或者整体switch结束。

执行步骤:
	1.先找到case入口: 先找到常量值和switch表达式值相同的case
    2.执行找到的case入口中的代码:
    	如果没有break,直接执行(不再判断下一个case中的常量值是否和switch表达式的值是否相同)下一个case中的代码,
        直到遇到break,结束switch语句
public class Demo03SwitchMonth {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);

        //2.获取键盘录入的一个1-12的整数数字(代表月份),保存到int变量month中
        System.out.println("请输入一个1-12的整数数字(代表月份): ");
        int month = sc.nextInt();

        //3.因为month中的数字有12+1中情况,使用switch语句对month中的值,进行判断,并输出不同的结果
        switch (month) {//9
            case 1:
            case 2:
            case 12:
                System.out.println("冬季");
                break;
            case 3:
            case 4:
            case 5:
                System.out.println("春季");
                break;
            case 6:
            case 7:
            case 8:
                System.out.println("夏季");
                break;
            case 9:
            case 10:
            case 11:
                System.out.println("秋季");
                break;
            default:
                System.out.println("您输入的月份不存在,哪个星球来的,哥屋恩...");
                break;
        }
        System.out.println("main....end....");//模拟格式中的其它语句
    }
}

注意点:

  • 0.表达式为byte,short,char,int,enum(jdk1.5+),String(jdk1.7+)
  • 1.break:中断,结束。省略则出现case穿透(根据需求灵活运用)
  • 2.case子句的值必须是常量

选择语句if和switch区别:

  • 1.switch建议判断固定值时用

如果判断的具体数值不多,而且符合byte、short 、char、int、String、枚举等几种类型。虽然两个语句都可以使用,建议使用swtich语句。因为效率稍高。

  • 2.if建议判断区间或范围时用
  • 3.使用switch-case的,都可以改写为if-else。反之不成立,布尔型,浮点型switch都用不了的,if范围比较大。

循环结构

循环概述

圆周率: 无限不循环小数 3.1415926...
10除以3的结果: 3.33333... 无限循环小数

循环的概念: 重复性的执行某些固定的功能,当条件不成立时,结束循环
说白了: 条件成立执行操作,条件不成立停止操作

循环组成

1.循环的组成(手写100遍HelloWorld案例): :
	(1)【初始化表达式1】准备工作:笔墨伺候,最优先唯一执行一次的操作
	(2)【循环条件2】条件判断:每次书写前,判断一下,要不要写
    (3)【循环体3】循环所要进行的操作:手写一个HelloWorld案例 
	(4)【步进表达式4】扫尾的工作:每写一次HelloWorld,计数(+1)

2.执行流程:
	1,2(循环条件: true),3,4 --> 2(循环条件: true),3,4 --> ... --> 
        直到布尔表达式2(循环条件: false),结束循环,执行循环后面的其它语句

3.循环的分类:
	(1)for循环【最最常用】
    (2)while循环【一般常用】
    (3)do-while循环【不常用】

for

保证条件有边界,否则会死循环

for循环语句介绍

1.for循环格式:
	for(初始化表达式1;布尔表达式2;步进表达式4){
        循环体3;
    }

2.执行流程:
	1,2(循环条件: true),3,4 --> 2(循环条件: true),3,4 --> ... --> 
        直到布尔表达式2(循环条件: false),结束循环,执行循环后面的其它语句

3.嵌套循环:
	1.总共的循环次数=外循环次数*内循环次数
	2.内层循环遍历一遍,只相当于外层循环体执行了一次
	3.外层循环控制行数,内层循环控制列数

图解分析:

image-20220728091222679

for循环练习1

需求:
	在控制台输出5次HelloWorld 
public class Demo01ForHello {
    public static void main(String[] args) {
        System.out.println("HelloWorld.....1");
        System.out.println("HelloWorld.....2");
        System.out.println("HelloWorld.....3");
        System.out.println("HelloWorld.....4");
        System.out.println("HelloWorld.....5");
        System.out.println("----------------------");

        //使用for循环
        //count: 1,2,3,4,5  6<=5 --> false --> 结束for循环
        for (int count = 1; count <= 5; count++) {
            System.out.println("HelloWorld....."+count);
        }
        System.out.println("----------------------");

        //使用for循环
        //times: 0,1,2,3,4    5<5 --> false --> 结束for循环
        for (int times = 0; times < 5; times++) {
            System.out.println("HelloWorld....."+times);
        }

        System.out.println("main.......end............");
    }
}

for循环练习2

需求:
	在控制台输出1-5和5-1的数据
        
public class Demo02ForPrint5 {
    public static void main(String[] args) {
        //for循环输出1-5
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
        }
        System.out.println("----------");

        //for循环输出5-1
        for (int j = 5; j >= 1; j--) {
            System.out.println(j);
        }

        System.out.println("main....end....");
    }
}

for循环练习3

需求:
	求1-5之间的数据和,并把求和结果在控制台输出
        
实现步骤:
	1.定义int变量sum,用来求和,初始值0
	2.使用for循环获取1-5的数字
	2.1把当前数字累加到求和变量中
	3.打印求和变量sum
        
public class Demo03ForSum5 {
    public static void main(String[] args) {

        //1.定义int变量sum,用来求和,初始值0
        int sum = 0;

        /*
            第一次: sum = 0   i = 1
                 i <= 5 --> 1<=5 --> true --> sum = sum + i = 0 + 1 = 1  i = 2

            第二次: sum = 1   i = 2
                i <= 5 --> 2<=5 --> true --> sum = sum + i = 1 + 2 = 3  i = 3

            第三次: sum = 3   i = 3
                i <= 5 --> 3<=5 --> true --> sum = sum + i = 3 + 3 = 6  i = 4

            第四次: sum = 6   i = 4
                i <= 5 --> 4<=5 --> true --> sum = sum + i = 6 + 4 = 10  i = 5

            第五次: sum = 10   i = 5
                i <= 5 --> 5<=5 --> true --> sum = sum + i = 10 + 5 = 15  i = 6

            第六次: sum = 15   i = 6
                i <= 5 --> 6<=5 --> false --> 结束for循环,执行for后面的代码
         */

        //2.使用for循环获取1-5的数字
        for (int i = 1; i <= 5; i++) {
            //2.1把当前数字累加到求和变量sum中
            sum = sum + i;
        }

        //3.打印求和变量sum
        System.out.println("1-5的数字之和: "+sum);
    }
}

图解分析:

image-20200426102848056

for循环练习4

需求:
	求1-100之间的偶数和,并把求和结果在控制台输出
        
实现步骤:
	1.定义int变量sum,用来累加求和,初始值0
    2.使用for获取1-100之间的数字
    2.1判断如果当前数字是偶数,把当前数字累加到求和变量sum中
    3.for循环结束,打印求和变量sum的值
        
public class Demo04ForSum100 {
    public static void main(String[] args) {
        //1.定义int变量sum,用来累加求和,初始值0
        int sum = 0;

        //2.使用for获取1-100之间的数字
        for (int i = 1; i <= 100; i++) {
            //2.1判断如果当前数字是偶数
            if (i % 2 == 0) {
                //把当前数字累加到求和变量sum中
                sum += i;
            }
        }
        //3.for循环结束,打印求和变量sum的值
        System.out.println("1-100之间的偶数数字之和: "+sum);
        System.out.println("-------------");

        //1.定义int变量sum2,用来累加求和,初始值0
        int sum2 = 0;

        //2.使用for获取1-100之间的偶数数字
        for (int j = 0; j <= 100; j++,j++/*j+=2*/) {
            //2.1把当前数字(偶数)累加到求和变量sum2中
            //System.out.println(j);
            sum2 += j;
        }
        //3.for循环结束,打印求和变量sum的值
        System.out.println("1-100之间的偶数数字之和: "+sum2);

    }
}
	

for循环练习5

需求:
	键盘录入一个三位数字,输出该数字是否是水仙花数字?
        
解释:什么是水仙花数?
	水仙花数,指的是一个三位数[100,999],个位、十位、百位的数字立方和等于原数
	例如 153 3*3*3 + 5*5*5 + 1*1*1 = 27 + 125 + 1 = 153
        
实现步骤:
	1.创建键盘录入Scanner类的对象(1.导包 2.创建)
    2.获取键盘录入的一个三位整数数字,保存到int变量num中
    3.使用if判断如果num中的数字是三位数字
        3.1 计算num的个位,十位,百位 分别保存到3个int变量ge(个位),shi(十位),bai(百位)中
        3.2 计算个位,十位,百位数字的立方和,保存到int变量sum中
        3.3 判断如果三位数字num 等于 每位数字的立方和sum,输出num是一个水仙花数字
        3.4 判断如果三位数字num 不等于 每位数字的立方和sum,输出num不是一个水仙花数字
    4.如果num中的数字不是三位数字,提示"你输入的不是三位数字,所以不可能是水仙花数字"
        
public class Demo05ISSXH {
    public static void main(String[] args) {
        //1.创建键盘录入Scanner类的对象(1.导包 2.创建)
        Scanner sc = new Scanner(System.in);

        //2.获取键盘录入的一个三位整数数字,保存到int变量num中
        System.out.println("请录入一个三位整数数字: ");
        int num = sc.nextInt();

        //3.使用if判断如果num中的数字是三位数字
        if (num >= 100 && num <= 999) {
            //3.1 计算num的个位,十位,百位 分别保存到3个int变量ge(个位),shi(十位),bai(百位)中
            int ge = num%10;//个位
            int shi = num/10%10;//十位
            int bai = num/100%10;//百位

            System.out.println("个位: "+ge);
            System.out.println("十位: "+shi);
            System.out.println("百位: "+bai);

            //3.2 计算个位,十位,百位数字的立方和,保存到int变量sum中
            int sum = ge*ge*ge + shi*shi*shi + bai*bai*bai;

            //3.3 判断如果三位数字num 等于 每位数字的立方和sum,输出num是一个水仙花数字
            if(sum == num) {
                System.out.println(num+"是一个水仙花数字....");
            } else {
                //3.4 判断如果三位数字num 不等于 每位数字的立方和sum,输出num不是一个水仙花数字
                System.out.println(num+"不是一个水仙花数字....");
            }

        } else {
            //4.如果num中的数字不是三位数字,提示"你输入的不是三位数字,所以不可能是水仙花数字"
            System.out.println("你输入的不是三位数字,所以不可能是水仙花数字....哥屋恩....");
        }
    }
}
        

for循环练习6

需求:
	在控制台输出所有的“水仙花数”
        
解释:什么是水仙花数?
	水仙花数,指的是一个三位数,个位、十位、百位的数字立方和等于原数
	例如 153 3*3*3 + 5*5*5 + 1*1*1 = 27 + 125 + 1 = 153
        
实现步骤:
	1.使用for循环获取所有的三位数字,每个数字保存到int变量num中
    	1.1计算num中数字的个位,十位,百位 分别保存到3个int变量ge(个位),shi(十位),bai(百位)中
        1.2计算个位,十位,百位数字的立方和,保存到int变量sum中
        1.3判断如果三位数字num 等于 每位数字的立方和sum,输出该数字num

public class Demo06PrintSXH {
    public static void main(String[] args) {
        //1.使用for循环获取所有的三位数字,每个数字保存到int变量num中
        for (int num = 100; num <= 999; num++) {

            //1.1计算num中数字的个位,十位,百位 分别保存到3个int变量ge(个位),shi(十位),bai(百位)中
            //123
            int ge = num%10;//个位
            int shi = num/10%10;//十位
            int bai = num/100%10;//百位

            //1.2计算个位,十位,百位数字的立方和,保存到int变量sum中
            int sum = ge*ge*ge + shi*shi*shi + bai*bai*bai;
            //1.3判断如果三位数字num 等于 每位数字的立方和sum,输出该数字num
            if(sum == num) {
                System.out.println(num);
            }
        }
    }
}

for循环练习7

需求:
	在控制台输出所有的“水仙花数”及总个数
        
解释:什么是水仙花数?
	水仙花数,指的是一个三位数,个位、十位、百位的数字立方和等于原数
	例如 153 3*3*3 + 5*5*5 + 1*1*1 = 27 + 125 + 1 = 153
        
实现步骤:
	1.定义int变量count,初始值0,作用是统计水仙花数字的个数
    2.使用for循环遍历获取所有的三位数字,每个数字保存到int变量num中
    2.1计算num中数字的个位,十位,百位 分别保存到3个int变量ge(个位),shi(十位),bai(百位)中
    2.2计算个位,十位,百位数字的立方和,保存到int变量sum中
    2.3判断如果三位数字num 等于 每位数字的立方和sum,输出该数字num,同时计数器count的值增加1
    3.for循环结束后,打印count的值
        
public class Demo07CountSXH {
    public static void main(String[] args) {
        //1.定义int变量count,初始值0,作用是统计水仙花数字的个数
        int count = 0;

        //2.使用for循环遍历获取所有的三位数字,每个数字保存到int变量num中
        for (int num = 100; num <= 999; num++) {
            //2.1计算num中数字的个位,十位,百位 分别保存到3个int变量ge(个位),shi(十位),bai(百位)中
            int ge = num%10;//个位
            int shi = num/10%10;//十位
            int bai = num/100%10;//百位
            //2.2计算个位,十位,百位数字的立方和,保存到int变量sum中
            //2.3判断如果三位数字num 等于 每位数字的立方和sum
            if((ge*ge*ge+shi*shi*shi+bai*bai*bai) == num) {
                //输出该数字num
                System.out.println(num);
                //同时计数器count的值增加1
                count++;
            }
        }
        //3.for循环结束后,打印count的值
        System.out.println("水仙花数字总共有: "+count+" 个");
    }
}

计数思想:

image-20200426113750867

for循环练习8

// 数据反转
public class Demo07CountSXH {
    public static void main(String[] args) {
        /*
        循环中定义变量min=0最小索引
        max=arr.length‐1最大索引
        min++,max‐‐
        */
        for (int min = 0, max = arr.length ‐ 1; min <= max; min++, max‐‐) {
            //利用第三方变量完成数组中的元素交换
            int temp = arr[min];
            arr[min] = arr[max];
            arr[max] = temp;
        }
    }
}

while

算法有限性:不要忘记控制变量语句,否则会死循环

while循环语句介绍

1.while循环格式:
	初始化表达式1;
	while(布尔表达式2) {
        循环体3;
        步进表达式4;
    }
	其它语句;

2.执行流程:
	1,2(循环条件: true),3,4 --> 2(循环条件: true),3,4 --> ... --> 
        直到布尔表达式2(循环条件: false),结束循环,执行循环后面的其它语句

图解:

image-20220728091249076

while循环练习1

需求:
	在控制台输出5次HelloWorld
public class Demo01WhileHello {
    public static void main(String[] args) {
        //使用for
        for (int i = 1; i <= 5; i++) {
            System.out.println("HelloWorld...."+i);
        }
        System.out.println("-------------");

        //使用while
        int j = 1;
        while(j<=5) {//j: 1,2,3,4,5    6<=5 --> false --> 结束while循环
            System.out.println("HelloWorld~~~~~"+j);
            j++;
        }
        System.out.println("main......end.....");
    }
}

        

while循环练习2

需求:
    世界最高山峰是珠穆朗玛峰(8844.43米=8844430毫米),假如我有一张足够大的纸,它的厚度是0.1毫米。
	请问,我折叠多少次,可以折成珠穆朗玛峰的高度?
    折纸(折叠后的厚度是原有厚度的2倍,而不是平方的关系):
		原来: 0.1
		第一次: 0.2
        第二次: 0.4
        第三次: 0.8
        第四次: 1.6
        ...
                
实现步骤:
	0.定义2个double变量zf(珠峰的高度)和paper(纸张的厚度),并根据题目需求进行初始化
    1.定义int变量count,初始值0,作用用来记录折叠纸张的次数
    2.使用while循环,完成折叠纸张最终厚度达到珠峰的高度
       2.1循环条件: 只要折叠后的纸张厚度 小于 珠峰的高度 就必须继续折叠纸张
       2.2循环体: 折叠纸张(原有厚度的2倍: paper = paper*2;)  计数器count增加1
    3.while循环结束打印count的值
public class Demo02WhileZFCount {
    public static void main(String[] args) {
        //0.定义2个double变量zf(珠峰的高度)和paper(纸张的厚度),并根据题目需求进行初始化
        double zf = 8844430;//珠峰的高度
        double paper = 0.1;//纸张的厚度

        //1.定义int变量count,初始值0,作用用来记录折叠纸张的次数
        int count = 0;

        //2.使用while循环,完成折叠纸张最终厚度达到珠峰的高度
        //2.1循环条件: 只要折叠后的纸张厚度 小于 珠峰的高度 就必须继续折叠纸张
        //2.2循环体: 折叠纸张(原有厚度的2倍: paper = paper*2;)  计数器count增加1
        while (paper < zf) {
            //折叠纸张(原有厚度的2倍: paper = paper*2;)
            paper *= 2;
            //计数器count增加1
            count++;
            System.out.println("第"+count+"次折叠后纸张总厚度: "+paper);
        }

        //3.while循环结束打印count的值
        System.out.println("总共折叠纸张的次数: "+count);
        //1.34217728E7: 1.34217728*10^7 --> 13421772.8
        //珠峰高度:                         8844430
        System.out.println("最后折叠纸张的厚度: "+paper);
    }
}
           

do-while

至少执行一次(特别适合控制台打印菜单的应用场景)

do-while循环语句介绍

1.do-while循环格式:
	初始化表达式1;
	do {
        循环体3;
        步进表达式4;
    }while(布尔表达式2);

2.执行流程:
	1,3,4 -->  2(循环条件: true),3,4 --> ... --> 
        直到布尔表达式2(循环条件: false),结束循环,执行循环后面的其它语句

图解:

image-20220728091315609

do-while循环练习1

do-while循环练习:
	在控制台输出5次HelloWorld

public class Demo01DoWhileHello {
    public static void main(String[] args) {
        int i = 1;
        do {
            System.out.println("HelloWorld~~~~~"+i);
            i++;
        }while(i<=5);//2,3,4,5   6<=5: false,结束do-while循环,执行后面的语句
        System.out.println("main....end....");
    }
}

循环语句的区别

三种循环的区别总结
	1.建议使用的顺序:for,while,do-while 			
	2.循环次数确定的话,建议使用for,循环次数不确定建议使用while 【后面有使用场景】
    	循环次数不确定需要先写成死循环的格式【while好看】    --------后天讲解
	3.do-while循环来讲的话,至少执行一次 
	4.while和do-while循环而言,循环结束后,初始化条件中定义的变量可以继续使用, 
		但是for循环的不能使用(在for循环内部定义初始化语句)
    
public class Demo02Diff {
    public static void main(String[] args) {
        //3.do-while循环来讲的话,至少执行一次

        //for循环: 先判断条件,后执行循环体
        //for循环第一次布尔表达式都不成立(false): 循环体一次都不执行
        //第一次: i>5 --> 3>5 --> false 不执行循环体,直接执行for循环后面的输出语句
        for (int i = 3; i > 5; i++) {
            System.out.println("Hello...for...");
        }
        System.out.println("for...end...");

        //while循环: 先判断条件,后执行循环体
        //while循环第一次布尔表达式都不成立(false): 循环体一次都不执行
        //第一次: j>5 --> 3>5 --> false 不执行循环体,直接执行while循环后面的输出语句
        int j = 3;
        while (j > 5) {
            System.out.println("Hello...while...");
            j++;
        }
        System.out.println("while...end...");

        //do-while循环: 先执行循环体,再判断条件
        //do-while循环第一次布尔表达式都不成立(false): 循环体会至少执行一次(先执行,后判断条件)
        //第一次: 定义int变量k的值3,接着执行{}中的循环体,k的值变成4
        //再执行判断条件k>5 --> 4>5 --> false 结束do-while循环执行do-while后面的输出语句
        int k = 3;
        do{
            System.out.println("Hello...do-while...");
            k++;//4
        } while(k>5);//k>5 --> 4>5 --> false 结束do-while循环执行do-while后面的输出语句
        System.out.println("do-while...end...");
    }
}

public class Demo03Diff {
    public static void main(String[] args) {
        //4.while和do-while循环而言,循环结束后,初始化条件中定义的变量可以继续使用,
        //但是for循环的不能使用(在for循环内部定义初始化语句)
        for (int i = 1; i <= 3; i++) {
            System.out.println("Hello...for...in..."+i);
        }
        //错误: i是在for循环内部定义,只能在for循环内部使用
        //出了for循环,就不可以继续使用
        //System.out.println("Hello...for...out..."+i);
        System.out.println("Hello...for...out...");

        //while循环
        int j = 1;
        while (j <= 3) {
            System.out.println("Hello...while...in..."+j);
            j++;
        }
        //while循环: 初始化语句中定义的变量,while循环结束后仍然可以使用
        System.out.println("Hello...while...out..."+j);

        //do-while循环
        int k = 1;
        do{
            System.out.println("Hello...do-while...in..."+k);
            k++;
        } while(k<=3);
        //do-while循环: 初始化语句中定义的变量,do-while循环结束后仍然可以使用
        System.out.println("Hello...do-while...out..."+k);
    }
}


死循环

/*
     死循环
        1.概念: 永不休止的循环
        2.分类:
            (1)for循环的死循环格式      for芬芬
                for(;;){...}
                
                for (int j = 1; j > 0; j++) {
                    j--;
                }

            (2)while循环的死循环格式        建议使用
                while(true){...}
                
                int i = 0;
                while (i < 10) {
                    // 缺少控制变量语句
                }

            (3)do-while循环的死循环格式
                do{
                    ...
                }while(true);
 */
public class Demo04DeadLoop {
    public static void main(String[] args) {
        //for循环
        /*for (;true;) {
            System.out.println("Hello");
        }*/
        //for循环的死循环格式
        //不写布尔表达式: 默认就是true
        /*for (;;) {
            System.out.println("Hello");
        }*/

        //while循环的死循环格式
        /*int i = 0;
        while(i<3) {
            System.out.println("Hello");
        }*/
        /*while (true) {
            System.out.println("Hello");
        }*/

        //do-while循环的死循环格式
        /*int i = 0;
        do {
            System.out.println("Hello");
        }while(i<3);*/
        do {
            System.out.println("Hello");
        }while(true);
    }
}

循环跳转

break的介绍

break的使用场景:
	1.使用在switch语句中,用来结束switch语句,执行switch语句后面的其它语句
    2.使用在循环中,用来结束循环(1.本次循环的循环体中break后面的代码不再执行 2.剩余次数的循环也不再执行),
		执行循环后面的其它语句
    3.break不能使用在除switch和循环语句以外的其它位置

break的使用

public class Demo01Break {
    public static void main(String[] args) {
        System.out.println("顾客获取到了购买的四个包子..........");
        for (int num = 1; num <= 4; num++) {
            /*
                当num的值为2时,num == 2 --> true,if后面()中是true,执行if后面{}中的内容,
                首先: 执行输出语句
                其次: 执行break,一旦执行break,本次循环的循环体中break后面的代码不再执行,
                剩余次数的循环也不再执行,直接从循环中跳出到循环后面的其它代码继续执行

             */
            if (num == 2) {
                System.out.println("发现2号包子上有个大家伙(小强),2号(及剩余所有)包子不能吃了,找老板投诉");
                break;
            }
            System.out.println("顾客吃第"+num+"个包子......");
        }
        System.out.println("顾客投诉: 这是神马包子,岂有此理.老板: 非常抱歉,赔偿100000元~~~~~");
    }
}

图解分析:

image-20200514092950990

break的练习

public class Demo02Break {
    public static void main(String[] args) {

        for (int i = 1; i <= 10; i++) {
            /*
                当i的值是5的时候, i % 5 == 0 --> 5%5 == 0 --> true,
                if后面()中是true,执行if后面{}中的内容(只有break),
                一旦执行break,本次循环的循环体中break后面的代码不再执行,
                而且剩余次数的循环也不再执行,直接从循环中跳出(结束循环)到循环后面的其它语句
             */
            if (i % 5 == 0) {
                break;
            }
            System.out.println("HelloWorld~~~~~~~~~~~~~~~" + i);
        }
        System.out.println("main....end....");
    }
}

图解分析:

image-20200427094428938

break跳出嵌套循环

嵌套循环内层使用break只能跳出内层,要跳出外层需添加标记

默认跳出包裹此关键字最近的一层循环

a: while() {
    for() {
          if() {
            break a;
         }
     }
}

continue的介绍

continue的使用场景:
	1.只能使用在循环中,作用是提前结束本次循环,继续进行下一次循环
    2.不能使用在除循环结构中的其它位置

continue的使用

public class Demo03Continue {
    public static void main(String[] args) {
        System.out.println("顾客获取到了购买的四个包子..........");
        for (int num = 1; num <= 4; num++) {
            /*
                当num的值为2时,num == 2 --> true,if后面()中是true,执行if后面{}中的内容,
                首先: 执行输出语句
                其次: 执行continue,一旦执行continue,本次循环的循环体中continue后面的代码不再执行,
                继续执行下一次循环的步进表达式
                总结: continue的作用提前结束本次循环,继续进行下一次循环

             */
            if (num == 2) {
                System.out.println("把第2个包子弄脏了,不能吃第2个包子了,继续吃其它包子...");
                continue;
            }
            System.out.println("顾客吃第"+num+"个包子......");
        }
        System.out.println("找老板结账,交钱400块,老板说: 欢迎下次光临....");
    }
}

图解分析:

image-20200514100702686

continue的练习

public class Demo04Continue {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            /*
                当i的值是5和10的时候, i % 5 == 0 --> true,
                if后面()中是true,执行if后面{}中的内容(只有continue),
                一旦执行continue,本次循环的循环体中continue后面的代码不再执行,
                继续执行下一次循环的步进表达式
                总结: continue的作用提前结束本次循环,继续进行下一次循环(步进表达式)
             */
            if (i % 5 == 0) {
                continue;
            }
            System.out.println("HelloWorld....." + i);
        }
        System.out.println("main~~~~~~~end~~~~~~~~");
    }

}

图解分析:

image-20200514122234145

return

return:意味着方法结束,后面的代码不执行。不能说return是专门结束循环的,而是方法结束了,循环自然也结束了,不管多少层循环

循环嵌套

概念和格式

1.概念: 使用一个循环作为另外一个循环的循环体,外面的循环叫做外层循环,里面的循环叫做内层循环
2.格式(for嵌套):
	for(初始化表达式1;布尔表达式2;步进表达式7){//外层循环
        
        for(初始化表达式3;布尔表达式4;步进表达式6) {//内层循环
            内层循环的循环体5;
        }
        
    }
	其它语句;

3.执行流程:
	1,2(true:外层循环条件) -->
        3,4(true:内层循环条件),5,6 --> 4(true:内层循环条件),5,6 
        --> 直到4(false:内层循环条件),结束内层循环 
    7,2(true:外层循环条件) -->
        3,4(true:内层循环条件),5,6 --> 4(true:内层循环条件),5,6 
        --> 直到4(false:内层循环条件),结束内层循环 
    ...
    直到2(false:外层循环条件),结束外层循环,执行外层循环后面的其它语句
        

执行流程图解:

image-20220728091554983

1.需求:
	(1)教练安排运动员跑圈
	(2)教练总共安排3次,每次跑3圈
        
2.问题:
	1.外层循环 int i = 1 执行几次? 1次
    2.内层循环 int j = 1 执行几次? 3次
    3.内层循环的循环体执行几次?
    	外层循环的次数 * 内层循环每遍执行的次数 = 3 * 3 = 9
3.总结:
	外层循环执行1次,内层循环执行完整的(从初始化表达式开始)一遍
	
public class Demo01ForFor {
    public static void main(String[] args) {
        /*
            外层循环第一次: i = 1 i<=3 --> 1<=3 --> true --> 执行外层循环的循环体
                内层循环:
                    j: 1,2,3(3次执行内层循环的输出语句),
                    4 j<=3 --> 4<=3 --> false --> 结束内层循环,执行外层循环的步进表达式i++,i: 2

            外层循环第二次: i = 2 i<=3 --> 2<=3 --> true --> 执行外层循环的循环体
                内层循环:
                    j: 1,2,3(3次执行内层循环的输出语句),
                    4 j<=3 --> 4<=3 --> false --> 结束内层循环,执行外层循环的步进表达式i++,i: 3

            外层循环第三次: i = 3 i<=3 --> 3<=3 --> true --> 执行外层循环的循环体
                内层循环:
                    j: 1,2,3(3次执行内层循环的输出语句),
                    4 j<=3 --> 4<=3 --> false --> 结束内层循环,执行外层循环的步进表达式i++,i: 4

            外层循环第四次: i = 4 i<=3 --> 4<=3 --> false --> 结束外层循环,执行外层循环后面的其它语句

         */
        for(int i = 1;i<=3;i++){//外层循环: 控制趟/遍数
            System.out.println("教练第"+i+"次说: 你给我跑跑3圈");
            for(int j = 1;j<=3;j++){//内层循环: 控制每趟/遍执行的次数
                System.out.println(("           运动员跑第"+i+"次第" + j + "圈"));
            }
        }
        System.out.println("main...end...");
    }
}

循环嵌套概念图解:

image-20200427110457429

打印月份

需求:
	使用嵌套循环,打印2021年至2023年月份,格式:xxxx年x月
/*
    需求:
	    使用嵌套循环,打印2021年至2023年月份,格式:xxxx年x月

	总结:
        外层循环执行1次,内层循环执行完整的(从初始化表达式开始)一遍
 */
public class Demo02ForForMonth {
    public static void main(String[] args) {
        for (int year = 2021; year <= 2023; year++) {//外层循环: 控制年份 2021 2022 2023   3次

            for (int month = 1; month <= 12; month++) {//内层循环: 控制每年的月份,每年都有12个月,都是从1月开始
                //1,2...12: 12次
                System.out.println(year + "年" + month + "月");//执行次数:3*12 = 36次
            }
        }
        System.out.println("main....end....");
    }
}

图解分析:

image-20200427113418654

模拟钟表

需求:
	模拟钟表的时针和分针
	时针(外层循环)走一个格,分钟(内层循环)走一圈
	对应:
	外层循环执行一次,内层循环执行完整的一遍

/*
    需求:
        模拟钟表的时针和分针
        时针(外层循环)走一个格,分钟(内层循环)走一圈
        对应:
        外层循环执行一次,内层循环执行完整的一遍

 */
public class Demo03ForForClock {
    public static void main(String[] args) {

        for (int hour = 0; hour < 24; hour++) {//外层循环: 控制每天都有24小时

            for (int minute = 0; minute < 60; minute++) {//内层循环: 控制每小时都有60分钟

                System.out.println(hour+"点"+minute+"分");

            }
        }

        //System.out.println("main....end...");
    }
}

图解分析:

image-20200427114211716

随机数

随机数Random的介绍

1.概念:	java.util.Random类就是用来产生随机数字的,也是一种引用类型
2.随机数Random类的使用步骤:
	和Scanner的使用步骤是相同的
    (1)导包(找到我们要使用的东西)
        格式:
			import 包名.类名;
			import java.util.Random;
		快捷键:
			alt + 回车
        注意:
			a.java.lang包下的东西可以直接使用,不用导包
            b.当前类和要使用的类,处于同一个包中,也不用导包
     (2)创建对象
     	类名 对象名 = new 类名(...);
		类名: 之前写代码时,class关键字后面的名称
            
     	创建Scanner类的对象: Scanner sc = new Scanner(System.in);//其中: System.in 是固定写法
			

		创建Random类的对象: Random r = new Random();//其中: ()中什么都不用写,固定格式
     
	(3)使用  
        Scanner中获取键盘录入的整数: int num = sc.nextInt();
		Random中产生随机整数数字:
			r.nextInt(): 产生一个int范围内(正负21亿)的随机数字
			r.nextInt(int类型整数数字n): 产生一个0到n之间的随机数字(包含0,但是不包含n)
				其中: 圆括号()中的int类型整数数字n表示产生随机数字的上限范围
                    
	            
3.练习:
	(1)产生10个int范围内的整数数字
    (2)产生10个0到100之间(包含0,但是不包含100)的整数数字
    (3)产生10个1到100之间(包含1,包含100)的整数数字	
        [1,100] --> [0,99] + 1 --> [0,100) + 1 --> r.nextInt(100) + 1

        思考:
            产生10个66到178之间(包含66,包含178)的整数数字

Random类的使用

练习:
	(1)产生10个int范围内的整数数字
    (2)产生10个0到100之间(包含0,但是不包含100)的整数数字
    (3)产生2个1到5之间(包含1,包含5)的整数数字
    (4)产生10个1到100之间(包含1,包含100)的整数数字	
        
public class Demo01Random {
    public static void main(String[] args) {
        //创建Random类的对象
        Random r = new Random();

        //(1)产生10个int范围内的整数数字
        for (int i = 0; i < 10; i++) {
            //产生1个int范围内的整数数字
            int num = r.nextInt();
            System.out.println(num);
        }
        System.out.println("---------------");

        //(2)产生10个0到100之间(包含0,但是不包含100)的整数数字
        //r.nextInt(100): 产生0到100之间的1个随机数字,但是可以产生0,一定不会产生100(包含0,不包含100) [0,100) <==> [0,99]
        for (int i = 0; i < 10; i++) {
            //产生1个0到100之间(包含0,但是不包含100)的整数数字
            int num = r.nextInt(100);
            System.out.println(num);
        }
        System.out.println("---------------");

        //(3)产生2个1到5之间(包含1,包含5)的整数数字
        /*
            如果可以产生: 0,1,2,3,4           ==>[0,4] ==> [0,5) ==> r.nextInt(5)
            之后:                 +1         r.nextInt(5) + 1
            结果:        1,2,3,4,5
         */
        for (int i = 0; i < 2; i++) {
            //产生1个1到5之间(包含1,包含5)的整数数字
            int num = r.nextInt(5) + 1;
            System.out.println(num);
        }
        System.out.println("---------------");

        //(4)产生10个1到100之间(包含1,包含100)的整数数字
        /*
            先产生0到99的随机数字(包含0,包含99) --> r.nextInt(100)
            +1
            结果是1到100的随机数字(包含1,包含100) --> r.nextInt(100) + 1
         */
        for (int i = 0; i < 10; i++) {
            //产生1个1到100之间(包含1,包含100)的整数数字
            int num = r.nextInt(100) + 1;//[0,99]+1
            System.out.println(num);
        }
        System.out.println("---------------");
        //证明以上代码,确实可以产生1和100
        while(true) {
            //产生1个1到100之间(包含1,包含100)的整数数字
            int num = r.nextInt(100) + 1;//[0,99]+1
            System.out.println(num);
            if (num == 1 || num == 100) {
                break;
            }
        }
    }
}
/*
    练习:
        产生10个66到178之间(包含66,包含178)的整数数字
        [66,178] --> [0, 112] + 66 --> [0,113) + 66 --> r.nextInt(113) + 6
 */
public class Demo02Random {
    public static void main(String[] args) {
        //创建Random类的对象
        Random r = new Random();
        //产生10个66到178之间(包含66,包含178)的整数数字
        for (int i = 0; i < 10; i++) {
            //产生1个66到178之间(包含66,包含178)的整数数字
            int num = r.nextInt(113)+66;
            System.out.println(num);
        }
        System.out.println("-------------");
        //证明以上方式,是可以产生最小值66和最大值178的
        while (true) {
            //产生1个66到178之间(包含66,包含178)的整数数字
            int num = r.nextInt(113)+66;
            System.out.println(num);
            if (num == 66 || num == 178) {
                break;
            }
        }
    }
}

Random练习-猜数字

1.需求:程序自动生成一个1-100之间(包含1,包含100)的数字,使用程序实现猜出这个数字是多少?
    
2.效果:
	如果猜的数字比真实数字大,提示你猜的数据大了
	如果猜的数字比真实数字小,提示你猜的数据小了
	如果猜的数字与真实数字相等,提示恭喜你猜中了

3.使用的知识点:
    (1)使用产生随机数字的Random类(1.导包 2.创建对象 3.使用: nextInt(100)+1)
    (2)使用键盘录入Scanner类(1.导包 2.创建对象 3.使用: nextInt())
    (3)使用if语句的第三种格式,比较用户输入的数字和产生的随机数字的大小关系
    (4)用户多少次可以猜对,不确定,需要使用循环(死循环: while(true))
    (5)用户猜测正确后,需要停止循环,使用break  
4.实现步骤:
	(1)创建产生随机数字的Random类的对象
    (2)产生一个[1,100]之间的随机数字,保存到int变量guessNum中,以供用户猜测
    (3)创建键盘录入Scanner类的对象
    (4)以下步骤(5)-(6)是一个循环过程,因为用户多少次可以猜对,并不能确定,使用while(true)
    (5)获取用户猜测的通过键盘录入的数字,保存到int变量inputNum中
    (6)使用if语句的第三种个,对用户猜测的保存在inputNum中的数字 和 产生的保存在guessNum中的数字进行比较
        a.如果 inputNum 大于 guessNum 提示"你猜的数据大了"
        b.否则,如果 inputNum 小于 guessNum 提示"你猜的数据小了"
        c.否则,如果 inputNum 等于 guessNum 提示"恭喜你猜中了",并使用break结束循环
        注意: inputNum 等于 guessNum 条件 省略不写
	

图解分析:

image-20200427143245135

实现代码:

public class Demo03GuessNum {
    public static void main(String[] args) {
        //(1)创建产生随机数字的Random类的对象
        Random r = new Random();

        //(2)产生一个[1,100]之间的随机数字,保存到int变量guessNum中,以供用户猜测
        //[1,100] --> [0, 99] + 1 --> [0,100) + 1 --> r.nextInt(100) + 1
        int guessNum = r.nextInt(100) + 1;

        //(3)创建键盘录入Scanner类的对象
        Scanner sc = new Scanner(System.in);
        //(4)以下步骤(5)-(6)是一个循环过程,因为用户多少次可以猜对,并不能确定,使用while(true)
        while (true) {
            //(5)获取用户猜测的通过键盘录入的数字,保存到int变量inputNum中
            System.out.println("请输入您猜测的数字(1-100之间的整数):");
            int inputNum = sc.nextInt();

            //(6)使用if语句的第三种个,对用户猜测的保存在inputNum中的数字 和 产生的保存在guessNum中的数字进行比较
            if (inputNum > guessNum) {
                //a.如果 inputNum 大于 guessNum 提示"你猜的数据大了"
                System.out.println("你猜的数据大了");
            } else if (inputNum < guessNum) {
                //b.否则,如果 inputNum 小于 guessNum 提示"你猜的数据小了"
                System.out.println("你猜的数据小了");
            } else {
                //c.否则,如果 inputNum 等于 guessNum 提示"恭喜你猜中了",并使用break结束循环
                //注意: inputNum 等于 guessNum 条件 省略不写
                System.out.println("恭喜你猜中了");
                //使用break结束循环
                break;
            }
        }

    }
}

方法

方法入门

内存:每个方法在被调用执行的时候,都会进入栈内存,并且拥有自己独立的内存空间,方法内部代码调用完毕之后,会从栈内存中弹栈消失

方法引入

/*
    代码功能:模拟游戏中打怪物的场景

    发现问题:
        打怪物的代码是相同的,打了3次怪物,写了3次相同的代码,导致程序代码冗余,阅读性差

    怎么解决问题呢?
      1.能否将发射炮弹的代码,做成一个方法/功能
      2.使用的时候直接调用即可
	========================================================================
				这个代码大家不用写,直接拷贝
	========================================================================
 */
public class Demo01NoMethod {
    /*
        main是一个主方法,由JVM调用,是程序的入口
            1.public static:修饰符,目前固定写法
            2.void:返回值类型,表示方法内部的代码执行完毕,没有最终的结果,返回给调用者
            3.main:方法名称:固定写法
            4.String[] args:方法的参数
    */
    public static void main(String[] args) {
        System.out.println("游戏开始...");

        System.out.println("看到了一个怪物...血牙野猪...");
        System.out.println("准备发射5发炮弹");
        System.out.println("发射第1发炮弹* * * *");
        System.out.println("发射第2发炮弹* * * *");
        System.out.println("发射第3发炮弹* * * *");
        System.out.println("发射第4发炮弹* * * *");
        System.out.println("发射第5发炮弹* * * *");
        System.out.println("发射5发炮弹结束");
        System.out.println("...血牙野猪被打倒...");

        System.out.println("...走啊走啊走啊走...");
        System.out.println("看到了一个怪物...黄金虎鲨...");
        System.out.println("准备发射5发炮弹");
        System.out.println("发射第1发炮弹* * * *");
        System.out.println("发射第2发炮弹* * * *");
        System.out.println("发射第3发炮弹* * * *");
        System.out.println("发射第4发炮弹* * * *");
        System.out.println("发射第5发炮弹* * * *");
        System.out.println("发射5发炮弹结束");
        System.out.println("...黄金虎鲨被打倒...");

        System.out.println("...走啊走啊走啊走...");
        System.out.println("看到了一个怪物...吞天巨狼...");
        System.out.println("准备发射5发炮弹");
        System.out.println("发射第1发炮弹* * * *");
        System.out.println("发射第2发炮弹* * * *");
        System.out.println("发射第3发炮弹* * * *");
        System.out.println("发射第4发炮弹* * * *");
        System.out.println("发射第5发炮弹* * * *");
        System.out.println("发射5发炮弹结束");
        System.out.println("...吞天巨狼被打倒...");
        System.out.println("...走啊走啊走啊走...");

        System.out.println("游戏结束...");
    }
}
/*
    代码功能:模拟游戏中打怪物的场景

    发现问题:
        打怪物的代码是相同的,打了3次怪物,写了3次相同的代码,导致程序代码冗余,阅读性差

    怎么解决问题呢?
      1.能否将发射炮弹的代码,做成一个方法/功能
      2.使用的时候直接调用即可
	========================================================================
				这个代码大家不用写,直接拷贝
	========================================================================

	注意:
	    1.方法定义完毕后不调用不执行
	    2.调用格式:
            方法名称(...);
        3.方法可以调用任意多次
 */
public class Demo02UseMethod {
    /*
        main是一个主方法,由JVM调用,是程序的入口
            1.public static:修饰符,目前固定写法
            2.void:返回值类型,表示方法内部的代码执行完毕,没有最终的结果,返回给调用者
            3.main:方法名称:固定写法
            4.String[] args:方法的参数
    */
    public static void main(String[] args) {
        /*int num = 0;
        System.out.println(num);
        int[] arr = {10,20};
        System.out.println(arr);*/
        System.out.println("游戏开始...");

        System.out.println("看到了一个怪物...血牙野猪...");

        //调用方法,发射炮弹
        fire();

        System.out.println("...血牙野猪被打倒...");

        System.out.println("...走啊走啊走啊走...");
        System.out.println("看到了一个怪物...黄金虎鲨...");

        //调用方法,发射炮弹
        fire();

        System.out.println("...黄金虎鲨被打倒...");

        System.out.println("...走啊走啊走啊走...");
        System.out.println("看到了一个怪物...吞天巨狼...");

        //调用方法,发射炮弹
        fire();

        System.out.println("...吞天巨狼被打倒...");
        System.out.println("...走啊走啊走啊走...");

        System.out.println("游戏结束...");
    }
    /*
        模拟main方法,定义一个发射炮弹的方法fire
     */
    public static void fire() {
        System.out.println("准备发射5发炮弹");
        for (int i = 1; i <= 5; i++) {
            System.out.println("发射第"+i+"发炮弹* * * *");
        }
        System.out.println("发射5发炮弹结束");

        return ;//结束方法,返回到方法的调用处
    }
}

图解:

image-20200516092757974

方法的概念和格式

1.方法的概念:
	就是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集。
    将具有特殊功能的一段代码,使用大括号{}括起来,添加必要的修饰符,起个名字, 方便使用   
        
2.方法的格式:
	修饰符 返回值类型 方法名称(参数列表) {
    	功能代码;
        return ;//如果返回值类型是void,建议省略不写
    }

3.格式解释:
	(1)修饰符: public static 目前固定写法,先记住
    (2)返回值类型: 方法的功能代码执行完毕后,产生的需要返还给方法的调用者的结果数据的具体类型
        目前定义的方法没有返回值,返回值类型固定写为void
    (3)方法名称: 
		给方法起个名字(符合标识符的命名规范,小驼峰原则(第一个单词首字母小写,其它单词首字母大写)),方便使用
    (4)参数列表: 目前没有参数,不需要写参数列表,但是必须保留小括号()
    (5)功能代码: 完成特殊功能的一行/多行代码
    (6)return ;:
		a.结束方法
        b.返回到方法的调用处
            
4.方法定义步骤:
	1.明确返回值类型(有没有,什么类型)
	2.明确方法名(起名字好难的说-。-)(根据方法的功能,见名知意)
	3.明确参数列表(有没有,有几个,什么类型)

无返回值无参数方法的定义和调用

1.练习:
	定义方法method,方法内部输出一句话"我是一个方法"
        
/*
    无返回值无参数方法的定义和调用
        无参数:
            目前没有参数,不需要写参数列表,但是必须保留小括号()
        无返回值:
            目前定义的方法没有返回值,返回值类型固定写为void
            方法内部没有数据返还给调用者,没有返回值,返回值类型固定写为void

    1.练习:
	    定义方法method,方法内部输出一句话"我是一个方法"
    2.注意:
	    (1)方法定义完毕后不调用不执行
	    (2)调用格式:
            方法名称(...);
        (3)方法可以调用任意多次
 */
public class Demo03Method {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法
        method();

        System.out.println("main...end...");

    }

    //定义方法method,方法内部输出一句话"我是一个方法"
    public static void method() {

        System.out.println("我是一个方法");

        return ;//结束方法,返回到方法的调用处,建议省略
    }
}

无返回值无参数方法的调用图解

image-20200516095454283

无参数无返回值的方法练习-打印数字是否是偶数

需求:
	定义一个方法,打印输出该方法内部的数据(方法内部定义的变量)是否是偶数

/*
    无返回值无参数方法的定义和调用
        无参数:
            目前没有参数,不需要写参数列表,但是必须保留小括号()
        无返回值:
            目前定义的方法没有返回值,返回值类型固定写为void
            方法内部没有数据返还给调用者,没有返回值,返回值类型固定写为void

    1.练习:
	    定义一个方法,打印输出该方法内部的数据(方法内部定义的变量)是否是偶数

    2.注意:
	    (1)方法定义完毕后不调用不执行
	    (2)调用格式:
            方法名称(...);
        (3)方法可以调用任意多次

    练习中如果没有明确数据类型,默认int类型
 */
public class Demo01PrintOu {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法
        printOu();

        System.out.println("main...end...");

    }

    //定义一个方法,打印输出该方法内部的数据(方法内部定义的变量)是否是偶数
    public static void printOu() {

        int num = 12;

        boolean result = (num%2==0) ? true : false;

        System.out.println(num+"是偶数吗? "+result);

        return ;//结束方法,返回到方法的调用处,建议省略
    }
}

图解分析:

image-20200516101539927

无参数无返回值的方法练习-打印最大值

需求:
	定义一个方法,打印该方法内部的两个数据(方法内部定义的变量)的最大值
/*
    无返回值无参数方法的定义和调用
        无参数:
            目前没有参数,不需要写参数列表,但是必须保留小括号()
        无返回值:
            目前定义的方法没有返回值,返回值类型固定写为void
            方法内部没有数据返还给调用者,没有返回值,返回值类型固定写为void

    1.练习:
	    定义一个方法,打印该方法内部的两个数据(方法内部定义的变量)的最大值

    2.注意:
	    (1)方法定义完毕后不调用不执行
	    (2)调用格式:
            方法名称(...);
        (3)方法可以调用任意多次

    练习中如果没有明确数据类型,默认int类型
 */
public class Demo02PrintMax {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法
        printMax();

        System.out.println("main...end...");
    }

    //定义一个方法,打印该方法内部的两个数据(方法内部定义的变量)的最大值
    public static void printMax() {

        int a = 100, b = 200;

        int max = (a>b) ? a : b;

        System.out.println("最大值: "+max);

        return ;//结束方法,返回到方法的调用处,建议省略
    }

}

图解:

image-20200516102543118

方法详解

方法的格式详解

1.方法的概念:
	就是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集。
    将具有特殊功能的一段代码,使用大括号{}括起来,添加必要的修饰符,起个名字, 方便使用   
        
2.方法的格式:
	修饰符 返回值类型 方法名称(数据类型1 变量名称1,数据类型2 变量名称2...) {
    	功能代码;
        return 返回值;//如果return 后面 有具体的结果数据,不能省略return
    }
	注意:
		(1)方法没有参数,不用写参数列表,但是必须保留小括号()
        (2)方法内部没有返回值返还给调用者,返回值类型必须固定写为void

3.格式解释:
	(1)修饰符: public static 目前固定写法,先记住
    (2)返回值类型: 告诉方法的调用者,我方法结束后,给你一个什么类型的数据
		方法的功能代码执行完毕后,产生的需要返还给方法的调用者的结果数据的具体类型
        举例:
			如果方法内部返回整数数字100,返回值类型写为int
            如果方法内部返回小数数字6.6,返回值类型写为double
            如果方法内部返回true/false,返回值类型写为boolean
    (3)方法名称: 
		给方法起个名字(符合标识符的命名规范,小驼峰原则(第一个单词首字母小写,其它单词首字母大写)),方便使用
    
    (4)参数列表: 你调用我这个方法时,需要给我几个什么样子的数据
        就是在定义方法时,小括号()中定义了一个/多个变量    
		定义方法参数列表举例:
			(int a): 调用方法时,必须传递给方法一个int类型的数据
            (int a,int b): 调用方法时,必须传递给方法两个int类型的数据
            (double a,double b): 调用方法时,必须传递给方法两个double类型的数据
                
    (5)功能代码: 完成特殊功能的一行/多行代码
    (6)return 返回值/结果数据;:
		a.结束方法
        b.把return 后面的返回值/结果数据,返还到方法的调用处
            
4.方法的理解(工厂/机器)
    (1)生产汽车的工厂:
		原材料: 发动机,变速箱,车架...
        产出物: BMW750Li S600 ZT700
    (2)榨汁机:
		原材料: 水果,水,糖,冰块...
        产出物: 果汁(苹果汁,桃汁,梨汁...)  
    
    	原材料是进入工厂/机器的东西,相当于调用方法时传递的数据		参数列表
        产出物是从工厂/机器中出来的东西,相当于调用方法后的返回值		返回值 --- 返回值类型 
            
5.定义方法的三要素
	(1)方法名称: 
	(2)参数列表:
	(3)返回值类型:

6.注意:
	    (1)方法定义完毕后不调用不执行
	    (2)调用格式:
            方法名称(参数1,参数2...);
        (3)方法可以调用任意多次

带参数的方法练习-打印数字是否是偶数

需求:
	 定义一个方法,该方法接收一个int参数,方法内部打印输出该数据是否是偶数
/*
    带参数无返回值的方法练习
        方法是否需要参数:
            分析方法需求中是否具有不确定的东西

        方法是否需要返回值:
            如果方法需求中能够明确看到打印/输出二字,说明该方法不需要返回值,返回值类型固定写为void

    1.练习:
	    定义一个方法,打印一个整数数字是否是偶数

    2.三要素:
        (1)方法名称: printOu
	    (2)参数列表: int num
	    (3)返回值类型: void

    练习中如果没有明确数据类型,默认int类型
 */
public class Demo01PrintOu {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法: 传递的是常量
        printOu(11 );

        //调用方法方法: 传递变量
        int a = 12;
        printOu(a);

        System.out.println("main...end...");

    }

    //定义一个方法,打印一个整数数字是否是偶数
    /*
        你调用我的方法printOu时,必须给我传递一个int类型的数据,
        我printOu方法内部执行完毕后直接打印输出结果
        你: 方法的调用者
        我: 方法本身
        有进无出
     */
    public static void printOu(int num) {

        boolean result = (num%2==0) ? true : false;

        System.out.println(num+"是偶数吗? "+result);

        return ;//结束方法,返回到方法的调用处
    }
}

图解:

image-20200516112242265

带参数的方法练习-打印最大值

需求:
	 定义一个方法用于打印两个int数中的较大数,数据来自于方法参数
/*
    带参数无返回值的方法练习
        方法是否需要参数:
            分析方法需求中是否具有不确定的东西

        方法是否需要返回值:
            如果方法需求中能够明确看到打印/输出二字,说明该方法不需要返回值,返回值类型固定写为void

    1.练习:
	    定义一个方法用于打印两个int数字中的较大数,数据来自于方法参数

    2.三要素:
        (1)方法名称: printMax
	    (2)参数列表: int a,int b
	    (3)返回值类型: void

    练习中如果没有明确数据类型,默认int类型
 */
public class Demo02PrintMax {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法: 传递的是常量
        printMax(10,20);

        //调用方法方法: 传递变量
        int m = 100;
        int n = 200;
        printMax(m,n);


        System.out.println("main...end...");

    }

    //定义一个方法用于打印两个int数字中的较大数,数据来自于方法参数
    /*
        你调用我的方法printMax时,必须给我传递两个int类型的数据,
        我printMax方法内部执行完毕后直接打印输出最大值的结果
        你: 方法的调用者
        我: 方法本身
     */
    public static void printMax(int a, int b) {

        int max = (a>b) ? a : b;

        System.out.println("最大值: "+max);

        return ;//结束方法,返回到方法的调用处
    }
}

图解:

image-20200516113747053

带返回值的方法练习-获取数字是否是偶数

需求:
	定义一个方法,该方法接收一个int参数,判断该数据是否是偶数,并返回真假值
/*
    有参数有返回值的方法练习
        方法是否需要参数:
            分析方法需求中是否具有不确定的东西

        方法是否需要返回值:
            如果方法需求中能够明确看到打印/输出二字,说明该方法不需要返回值,返回值类型固定写为void
            如果方法需求中能够明确看到获取/返回/判断二字,说明该方法需要返回值,必须定义具体的返回值类型

    1.练习:
	    定义一个方法,该方法接收一个int参数,判断该数据是否是偶数,并返回真假值

    2.三要素:
        (1)方法名称: getOu
	    (2)参数列表: int num
	    (3)返回值类型: boolean

    练习中如果没有明确数据类型,默认int类型
 */
public class Demo01GetOu {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法: 传递的是常量
        boolean result = getOu(11);

        System.out.println("11是偶数吗? "+result);

        //调用方法方法: 传递变量
        int a = 12;
        result = getOu(a);
        System.out.println(a+"是偶数吗? "+result);

        System.out.println("main...end...");

    }

    //定义一个方法,该方法接收一个int参数,判断该数据是否是偶数,并返回真假值
    /*
        你调用我的方法getOu时,必须给我传递一个int类型的数据,
        我getOu方法内部执行完毕后,会返还给你一个boolean类型的数据
        你: 方法的调用者
        我: 方法本身
        有进(参数)有出(返回值)
     */
    //boolean: 告诉方法的调用者,方法功能代码执行完毕后,会返回数据的具体类型(会返回一个什么样子的数据)
    public static boolean getOu(int num) {

        boolean result = (num % 2 == 0) ? true : false;

        return result;//结束方法,并且把result中的数据,返还给方法的调用处/者
    }
}

图解分析:

image-20200516120122043

带返回值的方法练习-获取最大值

需求:
	设计一个方法可以获取两个int数的较大值,数据来自于参数
/*
    有参数有返回值的方法练习
        方法是否需要参数:
            分析方法需求中是否具有不确定的东西

        方法是否需要返回值:
            如果方法需求中能够明确看到打印/输出二字,说明该方法不需要返回值,返回值类型固定写为void
            如果方法需求中能够明确看到获取/返回/判断二字,说明该方法需要返回值,必须定义具体的返回值类型

    1.练习:
	    设计一个方法可以获取两个int数的较大值,数据来自于参数

    2.三要素:
        (1)方法名称: getMax
	    (2)参数列表: int a,int b
	    (3)返回值类型: int

    练习中如果没有明确数据类型,默认int类型
 */
public class Demo02GetMax {

    public static void main(String[] args) {

        System.out.println("main...start...");

        //调用方法: 传递的是常量
        int result = getMax(100,200);
        System.out.println("100和200的最大值: "+result);

        //调用方法方法: 传递变量
        int a = 10, b = 20;
        int max = getMax(a,b);
        System.out.println(a+"和"+b+"的最大值: "+max);

        System.out.println("main...end...");

    }

    //设计一个方法可以获取两个int数的较大值,数据来自于参数
    /*
        你调用我的方法getMax时,必须给我传递两个int类型的数据,
        我getMax方法内部执行完毕后,会返还给你一个int类型的数据
        你: 方法的调用者
        我: 方法本身
        有进(参数)有出(返回值)
     */
    //int: 告诉方法的调用者,方法功能代码执行完毕后,会返回数据的具体类型(会返回一个什么样子的数据)
    public static int getMax(int a, int b) {

        int max = (a>b) ? a : b;

        return max;//结束方法,并且把max中的数据,返还给方法的调用处/者
    }
}


图解分析:

image-20200516142013131

方法的注意事项

/*
	方法的注意事项
        1.方法不能嵌套定义(在定义方法的内部又定义了其它方法),可以调用其它方法
        2.方法可以嵌套调用
        3.返回值类型,必须要和 return 语句返回的数据的类型要匹配,否则编译失败 。
        4.不能在 return 后面写代码, return 意味着方法结束,所有后面的代码永远不会执行,属于无效代码。
        5.void表示无返回值,可以省略return,也可以单独的书写return,后面不加数据
 */
/*
	方法的注意事项
        1.方法不能嵌套定义(在定义方法的内部又定义了其它方法),可以调用其它方法
        2.方法可以嵌套调用
        3.返回值类型,必须要和 return 语句返回的数据的类型要匹配,否则编译失败 。
        4.不能在 return 后面写代码, return 意味着方法结束,所有后面的代码永远不会执行,属于无效代码。
        5.void表示无返回值,可以省略return,也可以单独的书写return,后面不加数据
 */
public class Demo01Notice {
    public static void main(String[] args) {
        //1.调用getNum方法,获取一个int数字
        int num = getNum();
        //2.调用printNum方法,打印一个int数字
        printNum(num);

        //方法的嵌套调用
        //先执行getNum方法:获取一个整数数字
        //再把整数数字交给printNum方法做打印输出
        printNum(getNum());
    }
    //定义方法
    public static void method() {
        System.out.println("method....");
        //错误: 方法不能嵌套定义方法
        /*public static void show() {
            System.out.println("show....");
        }*/
    }

    //打印int数字
    public static void printNum(int num) {
        System.out.println(num);
    }
    //获取一个int数字
    public static int getNum() {
        return 100;
    }
}
/*
	方法的注意事项
        1.方法不能嵌套定义(在定义方法的内部又定义了其它方法),可以调用其它方法
        2.方法可以嵌套调用
        3.返回值类型,必须要和 return 语句返回的数据的类型要匹配,否则编译失败 。
        4.不能在 return 后面写代码, return 意味着方法结束,所有后面的代码永远不会执行,属于无效代码。
        5.void表示无返回值,可以省略return,也可以单独的书写return,后面不能加数据,写个分号
 */
public class Demo02Notice {
    public static void main(String[] args) {

    }

    //3.返回值类型,必须要和 return 语句返回的数据的类型要匹配,否则编译失败 。
    public static double getDoubleNum() {
        //return 6.6;//6.6就是一个double类型的数字
        //return 6.6F;//6.6F是一个float类型的数字,可以自动类型提升为double类型
        return 6;//6是一个int类型的数字,可以自动类型提升为double类型
    }

    //4.不能在 return 后面写代码, return 意味着方法结束,所有后面的代码永远不会执行,属于无效代码。
    public static int getMax(int a,int b) {
        if (a > b) {
            return a;
        } else {
            return b;
        }
        //System.out.println("getMax...end...");//错误的,return后面不能写其他代码
    }

    //5.void表示无返回值,可以省略return,也可以单独的书写return,后面不能加数据,写个分号
    public static void method() {
        System.out.println("method....");
        return ;//建议省略return
    }
}

有返回值的方法调用方式

/*
    有返回值的方法调用方式
        1.赋值调用: 把有返回值的方法的调用结果保存到对应的变量中              ----推荐使用----
            数据类型 变量名称 = 方法名称(参数...);

        2.输出/打印调用: 把有返回值的方法的调用结果直接交给输出语句
            System.out.println(方法名称(参数...));

        3.单独调用: 既不保存方法的结果,也没有对结果进行输出                   -----不推荐使用,没有意义-----
            方法名称(参数...);
 */
public class Demo01DYMethod {
    public static void main(String[] args) {
        System.out.println("main...start...");

        //1.赋值调用: 把有返回值的方法的调用结果保存到对应的变量中
        //数据类型 变量名称 = 方法名称(参数...);
        int result = getSum(10,20);
        //可以对结果数据做其它操作
        //result *= 100;
        System.out.println("和: "+result);

        //2.输出/打印调用: 把有返回值的方法的调用结果直接交给输出语句
        //System.out.println(方法名称(参数...));
        System.out.println(getSum(100,200));

        //3.单独调用: 既不保存方法的结果,也没有对结果进行输出
        getSum(5,10);

        System.out.println("main...end...");
    }

    //定义方法,获取2个int数字之和
    public static int getSum(int a, int b) {
        int sum = a + b;
        return sum;
    }
}

无返回值的方法调用方式

/*
    无返回值的方法调用方式

        1.单独调用: 既不保存方法的结果,也没有对结果进行输出            只能采用单独调用
            方法名称(参数...);

        2.赋值调用: 把有返回值的方法的调用结果保存到对应的变量中       不能赋值调用
            数据类型 变量名称 = 方法名称(参数...);

        3.输出/打印调用: 把有返回值的方法的调用结果直接交给输出语句    不能输出调用
            System.out.println(方法名称(参数...));
 */
public class Demo02DYMethod {
    public static void main(String[] args) {
        System.out.println("main...start...");
        //1.单独调用: 既不保存方法的结果,也没有对结果进行输出
        //方法名称(参数...);
        printSum(10,20);

        //2.赋值调用: 把有返回值的方法的调用结果保存到对应的变量中
        //数据类型 变量名称 = 方法名称(参数...);
        //int a = printSum(5,15);//错误的,int变量只能保存整数,但是printSum方法执行结束没有返回任何结果数据
        //void a = printSum(5,15);//错误的,void根本不是数据类型

        //3.输出/打印调用: 把有返回值的方法的调用结果直接交给输出语句
        //System.out.println(方法名称(参数...));
        //System.out.println(printSum(3,2));//错误: 因为printSum方法执行完毕后,没有任何结果返回

        System.out.println("main...end...");
    }

    //定义方法,打印2个int数字之和
    public static void printSum(int a, int b) {
        int sum = a + b;
        System.out.println("和: "+sum);
        return ;//结束方法,返回到方法的调用处,注意没有带回任何数据
    }
}

形式参数和实际参数的区别

/*
    形式参数和实际参数的区别
        1.形式参数:
            (1)概念: 定义方法时()中定义的参数(定义变量),叫做形式参数
            (2)特点:
                a.定义形式参数的时候,是没有值的
                b.当调用该方法时,形式参数才会有值

        2.实际参数:
            (1)概念: 调用方法时()中给出的参数(常量/变量),叫做实际参数
            (2)特点:
                a.调用方法时,()中写的数据(常量/变量)
                b.必须要有值才可以
 */
public class Demo03ParamDiff {
    public static void main(String[] args) {
        System.out.println("main...start...");
        /*
            2.实际参数:
                (1)概念: 调用方法时()中给出的参数(常量/变量),叫做实际参数
                (2)特点:
                    a.调用方法时,()中写的数据(常量/变量)
                    b.必须要有值才可以
         */
        //调用方法: 传递常量
        printSum(10,20);//10,20叫做实际参数

        //调用方法: 传递变量
        int m = 100,n = 200;

        printSum(m,n);//m,n叫做实际参数


        System.out.println("main...end...");
    }

    /*
        1.形式参数:
            (1)概念: 定义方法时()中定义的参数(定义变量),叫做形式参数
            (2)特点:
                a.定义形式参数的时候,是没有值的
                b.当调用该方法时,形式参数才会有值
     */
    //定义方法,打印2个int数字之和
    public static void printSum(int a, int b) {
        int sum = a + b;
        System.out.println("和: "+sum);
        return ;//结束方法,返回到方法的调用处,注意没有带回任何数据
    }
}

image-20200516151320201

image-20200516151436471

方法重载

一个类/接口中,方法名相同,参数不同(个数,顺序,类型)的方法,与返回值无关

方法重载的引入

需求:
	1.定义一个获取两个int数字之和的方法
    2.定义一个获取三个int数字之和的方法
    3.定义一个获取两个double数字之和的方法
    4.定义一个获取三个double数字之和的方法
定义一个获取两个int数字之和的方法
	三要素:
		1.方法名称:     getTwoIntNumSum
        2.参数列表:     int a,int b
        3.返回值类型:   int

发现问题:
	以下四个方法都是完成求和功能,但是参数列表是互不相同的,
	但是我们给每个方法起了一个相当之复杂的名字,
    导致程序员学习和使用方法的成本增加(记不住,太复杂)

解决方案:
    方法重载
public class Demo01MethodProblem {
    public static void main(String[] args) {
        //打印/输出调用方法: 传递常量
        System.out.println(getTwoIntNumSum(10,20));
        System.out.println(getThreeIntNumSum(10,20,30));
        System.out.println(getTwoDoubleNumSum(10.0,20.0));
        System.out.println(getThreeDoubleNumSum(10.0,20.0,30.0));
    }

    //1.定义一个获取两个int数字之和的方法
    public static int getTwoIntNumSum(int a, int b) {
        return a + b;
    }

    //2.定义一个获取三个int数字之和的方法
    public static int getThreeIntNumSum(int a, int b,int c) {
        return a + b + c;
    }

    //3.定义一个获取两个double数字之和的方法
    public static double getTwoDoubleNumSum(double a, double b) {
        return a + b;
    }

    //4.定义一个获取三个double数字之和的方法
    public static double getThreeDoubleNumSum(double a, double b,double c) {
        return a + b + c;
    }
}
	

方法重载的概念

方法重载
	1.概念:
		在同一个类中,多个功能相同,但是参数列表不同的多个方法,可以使用相同的名称,这种多个同名不同参的方法,
		可以同时存在一个类中的现象,就叫做方法重载
            
        比如:
        	比如某个类中已经有了一个名称为method的方法,还可以再定义名称为method的方法,
			但是要求这些名称为method的方法的参数列表必须不同

	2.作用/目的:
		(1)减少程序员的学习和使用成本(原来需要记住四个名称复杂的方法,现在只需要记住一个名称简单的方法)
        (2)减少了方法名称的数量

    3.调用
    	(1)根据名称找到对应的方法
        (2)根据参数的数量找到对应的方法
        (3)根据参数的类型确定最终要调用的方法
                (首先: 做类型完全匹配 其次: 完全匹配的找不到,再做自动类型提升的匹配)
    	
public class Demo02MethodOverLoad {
    public static void main(String[] args) {
        //打印/输出调用方法: 传递常量
        System.out.println(getSum(10,20));
        System.out.println(getSum(10,20,30));
        System.out.println(getSum(10.0,20.0));
        System.out.println(getSum(10.0,20.0,30.0));
    }

    //1.定义一个获取两个int数字之和的方法
    public static int getSum(int a, int b) {
        System.out.println("...两个int.....");
        return a + b;
    }

    //2.定义一个获取三个int数字之和的方法
    public static int getSum(int a, int b,int c) {
        System.out.println("...三个int.....");
        return a + b + c;
    }

    //3.定义一个获取两个double数字之和的方法
    public static double getSum(double a, double b) {
        System.out.println("...两个double.....");
        return a + b;
    }

    //4.定义一个获取三个double数字之和的方法
    public static double getSum(double a, double b,double c) {
        System.out.println("...三个double.....");
        return a + b + c;
    }
}



方法重载的注意事项

方法重载中参数列表不同有哪些情况?
	1.参数数量不同
    2.参数类型不同
    3.多个类型,顺序不同
public class Demo03OverLoadNotice {
    public static void main(String[] args) {
        method(10,10.0);
    }

    //1.此方法只有一个int类型参数
    public static void method(int a) {

    }
    //2.此方法只有两个int类型参数
    //方法2和方法1参数的数量是不同的,可以构成重载
    public static void method(int a,int b) {

    }
    //3.此方法只有一个double类型参数
    //方法3和方法2参数的数量是不同的,可以构成重载
    //方法3和方法1参数虽然都是只有一个,但是类型不同,可以构成重载
    public static void method(double a) {

    }

    //4.此方法有一个int类型参数和一个double类型参数
    public static void method(int a,double b){

    }
    //5.此方法有一个double类型参数和一个int类型参数
    //方法5和方法4,虽然参数都是2个,但是类型的顺序不同
    public static void method(double a,int b){

    }
}

方法重载与哪些因素无关?
	1.与参数的名称无关
    2.与返回值类型无关
    3.与修饰符无关

总结:
    在多个方法同名的前提下,
    只看多个方法的参数(除了名称以外)有区别,就构成重载
        
public class Demo04OverLoadNotice {
    public static void main(String[] args) {

    }

    //1.此方法只有一个int类型参数
    public static void method(int a) {

    }
    //2.此方法只有一个int类型参数
    //方法2和方法1,只有参数的名称不同,无法构成重载
    /*public static void method(int num) {

    }*/
    //3.此方法只有一个int类型参数
    //方法3和方法1,只有返回值类型不同,无法构成重载
    /*public static int method(int a) {
        return 0;
    }*/

    //4.此方法只有一个int类型参数
    //方法4和方法1,只有修饰符不同,无法构成重载
    /*void method(int a) {

    }*/
}

方法重载的练习-比较两个数据是否相等

需求:
	使用方法重载的思想,设计比较两个数据是否相等的方法,兼容全整数类型(byte,short,int,long)

实现步骤:
	1.使用方法重载的思想,定义比较两个byte数据的方法compare
    2.使用方法重载的思想,定义比较两个short数据的方法compare
    3.使用方法重载的思想,定义比较两个int数据的方法compare
    4.使用方法重载的思想,定义比较两个long数据的方法compare
    5.分别调用以上四个方法

使用方法重载的思想,定义比较两个byte数据是否相同的方法compare
三要素:
	1.方法名称:    compare
    2.参数列表:    byte a,byte b
    3.返回值类型:  boolean
public class Demo05OverLoadTest {
    public static void main(String[] args) {
        //5.分别调用以上四个方法
        System.out.println(compare(10,20));
        System.out.println(compare((byte)10,(byte)20));
        System.out.println(compare((short)10,(short)20));
        System.out.println(compare(10L,20L));
    }

    //1.使用方法重载的思想,定义比较两个byte数据的方法compare
    public static boolean compare(byte a, byte b) {
        System.out.println("...两个byte...");
        boolean result = (a == b) ? true : false;
        return result;
    }

    //2.使用方法重载的思想,定义比较两个short数据的方法compare
    public static boolean compare(short a, short b) {
        System.out.println("...两个short...");
        boolean result;
        if (a == b) {
            result = true;
        } else {
            result = false;
        }
        return result;
    }

    //3.使用方法重载的思想,定义比较两个int数据的方法compare
    public static boolean compare(int a, int b) {
        System.out.println("...两个int...");
        if (a == b) {
            return true;
        } else {
            return false;
        }
    }

    //4.使用方法重载的思想,定义比较两个long数据的方法compare
    public static boolean compare(long a, long b) {
        System.out.println("...两个long...");
        return a == b;
    }
}
                                      

方法的参数传递

方法参数传递

参数传递:
	可以理解当我们要调用一个方法时,我们会把指定的数值,传递给方法中的参数(定义方法时()中定义的变量),
    这样方法中的参数就拥有了这个指定的值,可以使用该值,在方法中运算了。这种传递方式,我们称为参数传递。
        
形式参数: 定义方法时,()中定义的的变量
实际参数: 调用方法时,()中传入给方法的数值/变量

/*
    注意:
        1.使用=进行赋值的特点:
            把基本类型变量a的值赋值给基本类型变量b时,其实是把a中的值复制一份给变量b,
            之后不管如何修改变量b中的值,都不会影响变量a中的值

        2.变量的作用范围:
            方法内部定义的变量只在所定义的方法内有效(可以使用),出了方法的作用范围,就不能使用了
            局部变量: 方法内部定义的变量或者方法定义时()中定义的变量

 */
public class Demo01Var {
    public static void main(String[] args) {
        //定义int变量a,并初始化
        int a = 20;

        //定义int变量b,未初始化
        int b;

        b = a;//把变量a中的值赋值给变量b

        System.out.println("a="+a);//20
        System.out.println("b="+b);//20

        b = b*10;

        System.out.println("a="+a);//20
        System.out.println("b="+b);//200

        method();

        //System.out.println(num);//错误: num是在method方法内部定义,只在method方法内部有效
    }

    public static void method(/*int m*/) {
        int num = 100;
        System.out.println(num);
        //System.out.println(a);//错误: a是在main方法内部定义,只在main方法内部有效
    }
}
	

图解:

image-20200504104632367

基本类型作为方法参数传递

/*
    基本数据类型作为方法参数
    注意:
        1.基本类型变量: 保存的是具体的数据值
        2.基本类型变量作为形式参数,
            形式参数的改变,不会影响实际参数


    基本类型变量作为形式参数:
        定义方法时,()中定义的参数属于基本类型

    不会影响实际参数: 调用方法时,()中给出的参数
 */
public class Demo02BaseVar {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println("ms...a="+a);//10
        System.out.println("ms...b="+b);//20
        //调用方法
        change( a , b );
        System.out.println("me...a="+a);//10
        System.out.println("me...b="+b);//20
    }

    public static void change(int a, int b) {
        System.out.println("cs...a="+a);//10
        System.out.println("cs...b="+b);//20
        a = a*10;
        b = b*10;
        System.out.println("ce...a="+a);//100
        System.out.println("ce...b="+b);//200
    }
}

图解:

image-20200504105826431

image-20200518111321501

引用类型作为方法参数传递

/*
    引用数据类型作为方法参数
        注意:
            1.引用类型变量: 保存的是对象在堆内存空间的地址值,进行参数传递的时候,传递的也是地址值
            2.引用类型变量作为形式参数,通过形式参数找到对应的堆内存空间,修改堆内存空间的内容之后,
                通过实际参数看到的一定是修改后的堆内存空间的内容

                引用类型作为形式参数,形式参数的改变,会影响实际参数

        数组:
            1.数组也是一种引用类型: 数组名称保存的也是数组在堆内存空间的地址值
            2.数组作为方法参数或者返回值: 传递的都是数组在堆内存空间的地址值

 */
public class Demo03RefVar {
    public static void main(String[] args) {
        int[] arr = { 10 , 20 };
        //System.out.println(arr);//数组名称: 保存数组在内存中的地址值[I@1540e19d
        System.out.println("ms...arr[0]="+arr[0]);//10
        System.out.println("ms...arr[1]="+arr[1]);//20
        //调用方法
        change( arr );
        System.out.println("me...arr[0]="+arr[0]);//100
        System.out.println("me...arr[1]="+arr[1]);//200
    }

    public static void change(int[] arr ) {
        System.out.println("cs...arr[0]="+arr[0]);//10
        System.out.println("cs...arr[1]="+arr[1]);//20
        arr[0] = arr[0]*10;
        arr[1] = arr[1]*10;
        System.out.println("ce...arr[0]="+arr[0]);//100
        System.out.println("ce...arr[1]="+arr[1]);//200
    }
}

图解:

image-20200518113950171

方法的练习

数组遍历练习(不定义方法)

需求(先不定义方法):
	设计一个方法用于数组遍历(打印数组元素),
    要求遍历的结果是在一行上的。例如:[11, 22, 33, 44, 55]

举例:
	原数组: {11,22,33,44,55}
    打印格式:[11, 22, 33, 44, 55]
实现步骤:
	1.定义int数组array,并初始化
	2.打印"[",不换行
    3.使用for循环遍历数组
    3.1打印数组当前元素,不换行
    3.2如果步骤3.1中打印的元素不是最后一个元素,则需要打印", ",不换行
    4.打印"]",可以换行也可以不换行

问题:
	并没有把按照指定格式打印数组的功能定义成方法,
    导致有多少个数组需要按照指定格式打印,就需要重复性的写几遍同样的代码

解决方案:
	定义方法,实现数组按照指定格式打印
public class Demo01PrintArray {
    public static void main(String[] args) {
        //1.定义int数组array,并初始化
        int[] array = {11,22,33,44,55};
        //2.打印"[",不换行
        System.out.print("[");

        //3.使用for循环遍历数组
        for (int i = 0; i < array.length; i++) {
            //3.1打印数组当前元素,不换行
            System.out.print(array[i]);
            //3.2如果步骤3.1中打印的元素不是最后一个元素
            if(i != array.length-1) {
                //则需要打印", ",不换行
                System.out.print(", ");
            }
        }

        //4.打印"]",可以换行也可以不换行
        System.out.println("]");

        System.out.println("------------------");
        array = new int[] {100,200,300,500,800,900};

        //2.打印"[",不换行
        System.out.print("[");

        //3.使用for循环遍历数组
        for (int i = 0; i < array.length; i++) {
            //3.1打印数组当前元素,不换行
            System.out.print(array[i]);
            //3.2如果步骤3.1中打印的元素不是最后一个元素
            if(i != array.length-1) {
                //则需要打印", ",不换行
                System.out.print(", ");
            }
        }

        //4.打印"]",可以换行也可以不换行
        System.out.println("]");
    }
}
 

数组遍历练习(定义方法)

需求(定义方法):
        设计一个方法用于int数组遍历(打印数组元素),
        要求遍历的结果是在一行上的。例如:[11, 22, 33, 44, 55]

举例:
	原数组: {11,22,33,44,55}
    打印格式:[11, 22, 33, 44, 55]

定义方法,用来遍历int数组
	三要素:
    	1.方法名称:     printArray
        2.参数列表:     int[] array
        3.返回值类型:   void

打印int数组方法printArray的实现步骤
	1.打印"[",不换行
    2.使用for循环遍历数组
    2.1打印数组当前元素,不换行
    2.2如果步骤2.1中打印的元素不是最后一个元素,则需要打印", ",不换行
    3.打印"]",可以换行也可以不换行

main方法的实现步骤
	1.定义int数组array,并进行初始化
    2.调用printArray方法,传递数组变量array,完成数组按照指定格式打印

public class Demo02PrintArray {
    public static void main(String[] args) {
        //1.定义int数组array,并进行初始化
        int[] array = {11,22,33,44,55};//array中存储的是: 数组在堆内存空间的地址值

        //2.调用printArray方法,传递数组变量array,完成数组按照指定格式打印
        printArray(array);

        int[] array2 = {100,200,300,500,800,999,9999};
        printArray(array2);
    }

    //打印int数组方法printArray的实现步骤
    public static void printArray(int[] array) {
        //1.打印"[",不换行
        System.out.print("[");

        //2.使用for循环遍历数组
        for (int i = 0; i < array.length; i++) {
            //2.1打印数组当前元素,不换行
            System.out.print(array[i]);

            //2.2如果步骤2.1中打印的元素不是最后一个元素,则需要打印", ",不换行
            if (i != array.length - 1) {
                System.out.print(", ");
            }
        }
        //3.打印"]",可以换行也可以不换行
        System.out.println("]");
    }
}
 

方法练习求数组最大值

需求:
	设计一个方法用于获取int数组中元素的最大值

举例:
	原数组: {11,22,33,44,55}
	最大值: 55
        
三要素:
	1.方法名称:     getArrayMax
	2.参数列表:     int[] array
	3.返回值类型:   int

方法getArrayMax的实现步骤:
	1.假设索引0对应的元素是最大的,保存到int变量max中
	2.使用for循环依次获取后面的(从索引1开始)每个元素
	2.1只要当前元素值 大于 max,说明max中的值,已经不是最大的了
	2.2把当前元素值 赋值给 变量max
	3.for循环结束后,返回max

main方法实现步骤:
	1.定义int数组array,并初始化
	2.调用getArrayMax方法,传递数组array,获取最大值,保存到int变量max中
	3.打印最大值max
        
public class Demo03PrintArrayMax {
    public static void main(String[] args) {
        //1.定义int数组array,并初始化
        int[] array = {100,200,300,800,500};

        //2.调用getArrayMax方法,传递数组array,获取最大值,保存到int变量max中
        int max = getArrayMax(array);//数组名array: 存储数组在堆内存中的地址值

        //3.打印最大值max
        System.out.println("数组元素最大值: "+max);
    }

    //设计一个方法用于获取一个int数组中元素的最大值
    public static int getArrayMax(int[] array) {
        //1.假设索引0对应的元素是最大的,保存到int变量max中
        int max = array[0];

        //2.使用for循环依次获取后面的(从索引1开始)每个元素
        for (int i = 1; i < array.length; i++) {
            //2.1只要当前元素值 大于 max,说明max中的值,已经不是最大的了
            if (array[i] > max) {
                //2.2把当前元素值 赋值给 变量max
                max = array[i];
            }
        }
        //3.for循环结束后,返回max
        return max;
    }
}

引用类型变量作为方法参数方法调用图解

image-20200518142225434

Debug调试

Debug查看求和

使用步骤:
1.在代码的第一行左侧空白(行号后面)处单击鼠标左键,添加断点
2.右键选择"debug...",进入断点调试界面

public class Demo01DeBugSum {
    public static void main(String[] args) {

        System.out.println("main...start....");
        int a = 10;
        int b = 20;

        int sum = a + b;

        System.out.println("sum="+sum);

        System.out.println("main...end....");
    }
}

Debug查看求和方法

public class Demo02DeBug {
    public static void main(String[] args) {

        System.out.println("main...start...");

        method();

        System.out.println("main...end...");
    }

    public static void method() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
        }
        return ;
    }
}

image-20200410151007486

数组

数组定义和访问

容器的概念

变量中只能存储一个数据,新的数据进来,老的数据将被替换,如果需要存储多个数据,需要使用容器。

容器概念
	容器:是将多个数据存储到一起,每个数据称为该容器的元素。
    生活中的容器:水杯,衣柜,鞋柜,教室

数组:
	1.概念: java中的数组就是一个容器,可以存储多个数据,但是数据的类型必须一致
    2.特点:
    	(1)可以存储多个数据
        (2)多个数据的类型必须保持一致
        (3)数组一旦创建,长度是永远不可以发生改变
    	(4)创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址
    
public class Demo01Box {		//-----------------------此代码不用敲--------------------------------
    public static void main(String[] args) {
        //定义一个int变量num,并初始化
        //一个变量中只能存储一个数据
        int num = 10;
        System.out.println(num);

        //新的数据进来,老的数据将被替换
        num = 20;
        System.out.println(num);


        //比如我们班有80个学生需要参加考试,每个学生对应一个成绩(整数)
        //可以定义80个int变量
        int a = 80;
        int b = 90;
        int c = 70;
        int d = 50;
        int e = 85;
        int f = 95;
        int g = 59;
        int h = 63;

        //需要求总分
        int sum = a + b + c + d + e + f + g + h;

        //需要求平均分
        int avg = sum/80;

        //需要求最高分和最低分: 实现起来非常麻烦


    }
}
           

数组变量的定义

1.数组变量定义格式一:			---------------推荐使用-------------
    数据类型[] 数组名称;	

2.数组变量定义格式二:
    数据类型 数组名称[];
	

1.定义一个存储int类型数组的变量arrayA
2.定义一个存储double类型数组的变量arrayB
3.定义一个存储char类型数组的变量arrayC
    
public class Demo02Array {
    public static void main(String[] args) {
        //定义int变量num
        int num;
        //System.out.println(num);//错误:变量未初始化,不能使用

        //(1)定义一个存储int类型数组的变量arrayA
        //int类型数组: 该容器中只能存储的是int类型的数字
        int[] arrayA;
        //System.out.println(arrayA);//错误的: arrayA只是一个用来存储数组的变量,但是目前没有向arrayA中存储数组

        //(2)定义一个存储double类型数组的变量arrayB
        //double类型数组: 该数组容器中只能存储double类型的数据
        double arrayB[];

        //(3)定义一个存储char类型数组的变量arrayC
        //char类型数组: 该数组容器中只能存储char类型的数据
        char[] arrayC;
    }
}

图解:

image-20200515100251817

数组的第一种初始化方式

1.创建一个int类型的数组,可以存储3个int数据,给该数组起个名称叫做arrayA
2.创建一个double类型的数组,可以存储7个double数据,给该数组起个名称叫做arrayB
3.创建一个char类型的数组,可以存储5个char数据,给该数组起个名称叫做arrayC
    
数组的第一种初始化方式(动态初始化: 指定数组的长度)
	1.格式:
    	数据类型[] 数组名称 = new 数据类型[长度];

    2.格式解释:
    	(1)左侧数据类型: 规定了数组中可以存储哪种类型的数据
        (2)左侧[]: 表示数组的意思
        (3)数组名称: 就是给数组起个名字(相当于门牌号),方便找到并使用数组
        (4)=: 表示赋值,把具体的=右边new出来的数组容器,
                存储到=左边的数组变量中,但是存储的是数组在内存空间的地址值
        (5)new: 创建数组容器的过程
        (6)右侧数据类型: 和左侧保持一致
        (7)右侧[]中长度: 是一个int数字,表示数组中可以存储数据的数量,也叫作数组长度

3.练习:
	(1)创建一个int类型的数组,可以存储3个int数据,给该数组起个名称叫做arrayA
    (2)创建一个double类型的数组,可以存储7个double数据,给该数组起个名称叫做arrayB
    (3)创建一个char类型的数组,可以存储5个char数据,给该数组起个名称叫做arrayC
		
public class Demo03Array {
    public static void main(String[] args) {
        //(1)创建一个int类型的数组,可以存储3个int数据,给该数组起个名称叫做arrayA
        int[] arrayA = new int[3];

        //(2)创建一个double类型的数组,可以存储7个double数据,给该数组起个名称叫做arrayB
        double[] arrayB = new double[7];

        //(3)创建一个char类型的数组,可以存储5个char数据,给该数组起个名称叫做arrayC
        char[] arrayC = new char[5];
    }
}
            

数组的第二种初始化方式

数组的第二种初始化方式(标准格式静态初始化:指定数组中的每个元素)
	1.格式:
    	数据类型[] 数组名称 = new 数据类型[]{元素1,元素2,元素3,...元素n};

    2.注意:
    	(1)右侧[]中不能写长度: JVM会根据{}中元素的数量计算/推导出数组的长度
        	只要右侧[]中写长度,就会报错

        (2){}中的每个元素之间有逗号(英文状态)隔开,最后一个元素后面没有逗号

    3.练习:
    	(1)创建一个int类型的数组,可以存储多个int数据100,200,300,给该数组起个名称叫做arrayA
        (2)创建一个double类型的数组,可以存储多个double数据1.1,2.2,3.3,4.4,5.5,6.6,7.7,给该数组起个名称叫做arrayB
        (3)创建一个char类型的数组,可以存储多个char数据'真','的','好','想','你',给该数组起个名称叫做arrayC
 
public class Demo04Array {
    public static void main(String[] args) {
        //(1)创建一个int类型的数组,可以存储多个int数据100,200,300,给该数组起个名称叫做arrayA
        int[] arrayA = new int[]{100,200,300};

        //(2)创建一个double类型的数组,可以存储多个double数据1.1,2.2,3.3,4.4,5.5,6.6,7.7,给该数组起个名称叫做arrayB
        double[] arrayB = new double[]{1.1,2.2,3.3,4.4,5.5,6.6,7.7};

        //(3)创建一个char类型的数组,可以存储多个char数据'真','的','好','想','你',给该数组起个名称叫做arrayC
        char[] arrayC = new char[]{'真','的','好','想','你'};

    }
}     
            

数组的第三种初始化方式

数组的第三种初始化方式(简化格式静态初始化:指定数组中的每个元素)
	1.格式:
    	数据类型[] 数组名称 = {元素1,元素2,元素3,...元素n};

    2.练习:
    	(1)创建一个int类型的数组,可以存储多个int数据100,200,300,给该数组起个名称叫做arrayA
        (2)创建一个double类型的数组,可以存储多个double数据1.1,2.2,3.3,4.4,5.5,6.6,7.7,给该数组起个名称叫做arrayB
        (3)创建一个char类型的数组,可以存储多个char数据'真','的','好','想','你',给该数组起个名称叫做arrayC

    3.注意:
    	(1)右侧不用写长度,JVM根据{}中数组元素的数量推导长度
        (2)虽然没有写new,底层仍然有new的过程
        (3)动态初始化和标准格式的静态初始化,可以分两步完成
        (4)简化格式静态初始化,不能分成两步完成
	
public class Demo05Array {
    public static void main(String[] args) {
        //(1)创建一个int类型的数组,可以存储多个int数据100,200,300,给该数组起个名称叫做arrayA
        int[] arrayA = {100,200,300};

        //(2)创建一个double类型的数组,可以存储多个double数据1.1,2.2,3.3,4.4,5.5,6.6,7.7,给该数组起个名称叫做arrayB
        double[] arrayB = {1.1,2.2,3.3,4.4,5.5,6.6,7.7};

        //(3)创建一个char类型的数组,可以存储多个char数据'真','的','好','想','你',给该数组起个名称叫做arrayC
        char[] arrayC = {'真','的','好','想','你'};

        //定义int变量,未赋值
        int num;
        num = 10;//赋值
        System.out.println(num);//10
        num = 100;//重新赋值
        System.out.println(num);//100
        System.out.println("-------------------");
        //定义数组变量,未赋值
        int[] arr;
        arr = new int[3];//动态初始化指定数组长度
        arr = new int[]{10,20,30};//标准格式静态初始化

        System.out.println("-------------------");
        int[] arr2;
        //arr2 = {100,200,300};//错误: 简化格式静态初始化,不能分成两步完成

    }
}

数组的使用

简化格式静态初始化数组的使用
	1.数组名称: 代表数组在内存空间的地址值,是一个十六进制的整数数字
    2.索引编号:是一个整数数字
            数组中的每个数据,称之为数组元素
            数组为每个元素进行编号(专业术语叫做索引),该编号从0开始到最大值(数组长度 减 1)
	3.数组元素:
    	数组名称[索引编号]
        举例:
        	array[3]: 数组array中索引编号为3的元素
	4.获取数组长度:
    	数组内部有个length属性,专门记录数组的长度
        数组名称.length: 是一个int数字,代表数组的长度
            
public class Demo06ArrayUse {
    public static void main(String[] args) {
        /*int num = 10;
        System.out.println(num);//10*/

        //创建int数组array,元素分别为10,20,30
        int[] array = {10,20,30};//10的索引编号是0,20的索引编号是1,30的索引编号是2

        System.out.println(array);//[I@1540e19d: 数组在内存空间的地址值,是一个十六进制的整数数字

        //打印10
        System.out.println(array[0]);//打印数组array中索引编号为0的元素值: 10

        //打印20
        System.out.println(array[1]);//打印数组array中索引编号为1的元素值: 20

        //打印30
        System.out.println(array[2]);//打印数组array中索引编号为2的元素值: 30

        System.out.println("--------------");

        //把10 修改成 100
        array[0] = 100;//把整数数字100,存储到数组array中索引编号为0的元素中

        //把20 修改成 200
        array[1] = 200;//把整数数字200,存储到数组array中索引编号为1的元素中

        //把30 修改成 300
        array[2] = 300;//把整数数字200,存储到数组array中索引编号为2的元素中


        //打印100
        System.out.println(array[0]);//打印数组array中索引编号为0的元素值: 100

        //打印200
        System.out.println(array[1]);//打印数组array中索引编号为1的元素值: 200

        //打印300
        System.out.println(array[2]);//打印数组array中索引编号为2的元素值: 300

        //获取数组的长度
        int count = array.length;
        System.out.println("数组长度: "+count);
        System.out.println("数组长度: "+array.length);

        System.out.println(array[array.length-1]);//30
    }
}

注意点:

3.数组元素的默认初始化值,new出来在堆中的都有默认值

  • 数组元素是整形:0
  • 数组元素是浮点型:0.0
  • 数组元素是char型:0
(注意不是'0',而是数0,ASCII码或'\u0000',表现为空)
 if(char[0] == 0) true
  • 数组元素是boolean型,false
  • 数组元素是引用类型,null

4.索引:java中的索引基本上都是从0开始的

除了mysql的column是从1开始的。

5.数组是引用类型,数组型数据是对象[Object],数组中的每个对象相当于该对象的成员变量。(数组一经分配空间,元素按照成员变量同样的方式被隐式初始化)

6.int[] a = new int[0];a[0] = 10;// 运行报错,a没有长度,越界异常

7.数组简写形式不能作为实参,int[] a = {1,2};只能一次性定义时使用

把{1,2}当成形参就是int a; a = {1,2}; // 这样不行的
如果数组初始化中不使用运算符new。需要注意:下列写法是错误的。
int[] arr;
arr={1,2,3,4,5};

8.数组的最大索引值为(数组名.length-1)

数组应用场景

  • 如果需要存储大量的数据,例如如果需要读取100个数,那么就需要定义100个变量,显然重复写100次代码,是没有太大意义的。这时候就可以用数组。
  • 当元素较多时为了方便操作这些数组,会先进行来临时存储,所使用的容器就是数组。

数组原理内存图

java中的内存分配

1.方法区: 存储可以运行的class文件。
    	静态区(优先于对象创建,随类的加载而加载静态内容)
2.方法栈(stack): 方法运行时使用的内存,比如main方法运行,进入方法栈中执行。
3.堆内存(heap): 存储对象或者数组,new来创建的,都存储在堆内存。
    	常量池(String双引号的常量和new的常量位置不一样)
4.寄存器: 给CPU使用						 
5.本地方法栈: JVM在使用操作系统功能的时候使用,

内存结构简化图:

Image

图解:

image-20200515121702080

一个数组内存图

/*
    一个数组内存图

    注意:
	    1.数组名称保存数组在堆内存中的地址值
	    2.通过数组名称找到堆内存中的具体数组,然后通过索引编号找到对应的具体的某个元素

    数组元素有默认值:
        1.整数:       0
        2.小数:       0.0
        3.字符:       空白字符
        4.布尔:       false
 */
public class Demo01OneArray {
    public static void main(String[] args) {
        //创建int数组one,长度为2
        int[] one = new int[2];//第一个元素索引是0,第二个元素索引1

        System.out.println(one);//地址值:[I@1540e19d
        System.out.println(one[0]);//打印数组one中索引编号为0的元素的值,默认值: 0
        System.out.println(one[1]);//打印数组one中索引编号为1的元素的值,默认值: 1

        one[0] = 10;//把整数数字10,存储到数组one中索引编号为0的元素中,默认值0被覆盖
        one[1] = 20;//把整数数字20,存储到数组one中索引编号为1的元素中,默认值0被覆盖

        System.out.println(one);//地址值:[I@1540e19d
        System.out.println(one[0]);//打印数组one中索引编号为0的元素的值: 10
        System.out.println(one[1]);//打印数组one中索引编号为1的元素的值: 20
    }
}

图解:

image-20200515121737000

两个数组内存图

/*
    两个数组内存图(每个数组都有自己独立的内存空间,互不影响,互不干扰)
    
    注意:
	    1.数组名称保存数组在堆内存中的地址值
	    2.通过数组名称找到堆内存中的具体数组,然后通过索引编号找到对应的具体的某个元素

    有两套房子,一套在延庆(钥匙是one),一套在平谷(钥匙是two)
        one: 你自己拿着  打开的是延庆的房子
        two: 你对象拿着  打开的是平谷的房子  
 */
public class Demo02TwoArray {
    public static void main(String[] args) {
        //创建int数组one,长度为2
        int[] one = new int[2];//在北京延庆购买了一套两居室的房子,one是打开这套房子的钥匙

        System.out.println(one);//[I@1540e19d
        System.out.println(one[0]);//0
        System.out.println(one[1]);//0

        one[0] = 10;
        one[1] = 20;

        System.out.println(one);//[I@1540e19d
        System.out.println(one[0]);//10
        System.out.println(one[1]);//20
        System.out.println("--------------");

        //又创建一个int数组two,长度为2
        int[] two = new int[2];//在北京平谷购买了一套两居室的房子,two是打开这套房子的钥匙

        System.out.println(two);//[I@677327b6
        System.out.println(two[0]);//0
        System.out.println(two[1]);//0

        two[0] = 100;
        two[1] = 200;

        System.out.println(two);//[I@677327b6
        System.out.println(two[0]);//100
        System.out.println(two[1]);//200
    }
}

图解:省略,就是把一个对象的内存图复制两份,每个数组之间没有任何关系

两个变量指向一个数组

/*
    两个引用一个数组内存图
    
    注意1.:
	    1.数组名称保存数组在堆内存中的地址值
	    2.通过数组名称找到堆内存中的具体数组,然后通过索引编号找到对应的具体的某个元素

	注意2:
        1.数组名称保存数组在堆内存空间的地址值
        2.使用数组名进行赋值时,传递的是地址值
        3.使用数组名作为方法参数和返回值,传递的都是地址值  --------后面讲解

    有一套房子,在延庆,但是该房子有两把钥匙,分别是one和two
        one: 你自己拿着  打开的是延庆的房子
        two: 你对象拿着  打开的是延庆的房子

 */
public class Demo03SameArray {
    public static void main(String[] args) {
        //创建int数组one,长度为2
        int[] one = new int[2];//在北京延庆购买了一套两居室的房子,one是打开这套房子的钥匙

        System.out.println(one);//[I@1540e19d
        System.out.println(one[0]);//0
        one[0] = 10;
        System.out.println(one);//[I@1540e19d
        System.out.println(one[0]);//10
        System.out.println("--------------");

        /*
            数组变量one保存的是数组在内存空间的地址值,
            赋值给新的数组变量two,导致数组变量one和two保存相同的内存地址,
            操作的是同一个数组

            one相当于是延庆两居室的房子的钥匙,通过钥匙one又配了一把钥匙two
            这样:
                钥匙one和two,打开的都是延庆两居室的房子
                钥匙one: 你自己拿着
                钥匙two: 你对象拿着
         */
        int[] two = one;

        System.out.println(two);//[I@677327b6
        System.out.println(two[0]);//10
        two[0] = 100;

        System.out.println(two);//[I@677327b6
        System.out.println(two[0]);//100
        System.out.println(one[0]);//100
    }
}

图解:

image-20200515121914077

数组操作的常见问题

数组越界异常

/*
    数组操作的常见问题一:
        数组索引越界(超出了范围)异常
        1.问题描述: java.lang.ArrayIndexOutOfBoundsException类,数组索引越界异常类
        2.产生原因:
            使用索引编号范围数组元素时,给出的索引编号不存在(超出了范围)
            索引编号范围: 最小值是0,最大值是数组长度 减 1 (one.length - 1)
        3.解决方案:
            根据控制台的打印信息,找到出现问题的索引,进行修改
 */
public class Demo01ArrayProblem {
    public static void main(String[] args) {
        //创建int数组array,并初始化
        //int[] one = new int[]{100,200,300};
        int[] one = {100,200,300};//100的索引编号是0,200的索引编号是1,300的索引编号是2
        System.out.println(one);//数组名称one代表数组的内存地址值:[I@1540e19d
        System.out.println(one[0]);//100
        System.out.println(one[1]);//200
        System.out.println(one[2]);//300
        //System.out.println(one[5]);//索引5: 不存在,报出数组索引越界异常
    }
}

图解:

image-20200515151208671

数组空指针异常

/*
    数组操作的常见问题二:
        空指针异常
        1.问题描述: java.lang.NullPointerException类,空指针异常类
        2.产生原因:
            null是一个引用类型的常量,可以给任意类型引用变量赋值,
            当把null赋值给数组变量one之后,数组变量one将不再指向堆内存空间的任何数组,
            也就不可以通过one再访问数组元素,只要访问,报出空指针异常

        3.解决方案:
            (1)不要通过值为null的数组变量,访问数组元素
            (2)根据控制台打印的相关异常信息,找到数组变量是null的地方进行修改,
                让数组变量指向一个堆内存空间的数组,就可以访问数组元素了
 */
public class Demo02ArrayProblem {
    public static void main(String[] args) {
        //创建int数组array,并初始化
        //int[] one = new int[]{100,200,300};
        int[] one = {100,200,300};//100的索引编号是0,200的索引编号是1,300的索引编号是2
        System.out.println(one);//数组名称one代表数组的内存地址值:[I@1540e19d
        System.out.println(one[0]);//100
        System.out.println(one[1]);//200
        System.out.println(one[2]);//300

        one = null;
        System.out.println(one);//null
        //System.out.println(one[0]);//错误: 控制针异常
    }
}

	

图解:

image-20200515151340388

数组练习

数组遍历

数组遍历:
	就是将数组中的每个元素分别获取出来,就是遍历。遍历也是数组操作中的基石。
public class Demo01EachArray {
    public static void main(String[] args) {
        //创建int数组array,并初始化
        //10的索引编号是0,20的索引编号是1,30的索引编号是2,50的索引编号是3
        int[] array = {10,20,30,50/*,60,70,80,90,100*/};
        System.out.println(array[0]);
        System.out.println(array[1]);
        System.out.println(array[2]);
        System.out.println(array[3]);
        System.out.println("-----------");
        //发现: 以上代码重复,只有索引0到3是不同的
        //可以使用for循环打印0到3的数字
        for (int i = 0; i < 4; i++) {
            System.out.println("索引编号: "+i+", 对应的元素值: "+array[i]);
        }
        System.out.println("-----------");
        //发现: 以上for循环中数字4写死了,可以使用数组的长度代替
        for (int i = 0; i < array.length; i++) {
            //System.out.println("索引编号: "+i+", 对应的元素值: "+array[i]);
            System.out.println(array[i]);
        }

    }
}

求三个int数字的最大值

需求:
	求三个int数字的最大值

实现步骤:
	1.定义3个int变量a,b,c,并分别初始化
    2.假设变量a的值是最大的,把a保存到int变量max中
    3.如果变量b的值大于max,说明max中的值目前不是最大的,把b的值赋值给max
    4.如果变量c的值大于max,说明max中的值目前不是最大的,把c的值赋值给max
    5.打印max的值
public class Demo02Max {
    public static void main(String[] args) {
        //1.定义3个int变量a,b,c,并分别初始化
        int a = 100, b = 200,c = 300;

        //2.假设变量a的值是最大的,把a保存到int变量max中
        int max = a;

        //3.如果变量b的值大于max,说明max中的值目前不是最大的,把b的值赋值给max
        if (b > max) {
            max = b;
        }
        //4.如果变量c的值大于max,说明max中的值目前不是最大的,把c的值赋值给max
        if (c > max) {
            max = c;
        }
        //5.打印max的值
        System.out.println("最大值: "+max);
    }
}

图解:

image-20200515144417507

数组获取最大值元素

需求:
	求int数组元素最大值
        
实现步骤:
	1.创建int数组array,并初始化
    2.假设索引为0的元素是最大的,保存到int变量max中
    3.使用for循环依次获取后面的(从索引1开始)每个元素
    3.1如果当前元素 大于 max 说明max中的值已经不是最大的了
    3.2把当前元素赋值给变量max
    4.for循环结束后打印max的值
public class Demo03ArrayMax {
    public static void main(String[] args) {
        //1.创建int数组array,并初始化
        int[] array = {5,15,2000,10000,100,4000};

        //2.假设索引为0的元素是最大的,保存到int变量max中
        int max = array[0];

        //3.使用for循环依次获取后面的(从索引1开始)每个元素
        for (int i = 1; i < array.length; i++) {
            //3.1如果当前元素 大于 max 说明max中的值已经不是最大的了
            if (array[i] > max) {
                //3.2把当前元素赋值给变量max
                max = array[i];
            }

        }
        //4.for循环结束后打印max的值
        System.out.println("数组最大值: "+max);
    }
}

图解分析:

image-20200429152819315

执行流程:

image-20200515151009936

数组中的常见算法

  1. 数组元素的赋值(杨辉三角、回形数等)
  2. 求数值型数组中元素的最大值、最小值、平均数、总和等
  3. 数组的复制、反转、查找(线性查找、二分法查找)
  4. 数组元素的排序算法

杨辉三角

public class $_01_杨辉三角 {
    public static void main(String[] args) {
        /**
         * 使用二维数组打印一个 10 行杨辉三角。
         * 【提示】
         * 1. 第一行有 1 个元素, 第 n 行有 n 个元素
         * 2. 每一行的第一个元素和最后一个元素都是 1
         * 3. 从第三行开始, 对于非第一个元素和最后一个元
         * 素的元素。即:
         * yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];
         */
        // 1.声明并初始化二维数组
        int[][] yangHui = new int[10][];

        // 2.给数组的元素赋值
        for (int i = 0; i < 10; i++) {
            yangHui[i] = new int[i + 1];
            // 2.1 首末元素赋值
            yangHui[i][0] = yangHui[i][i] = 1;
            // 2.2 非首末元素赋值
            //if (i > 1) {
            for (int j = 1; j < yangHui[i].length - 1; j++) {
                yangHui[i][j] = yangHui[i - 1][j - 1] + yangHui[i - 1][j];
            }
            //}
        }
 
        // 3.遍历二维数组
        for (int i = 0; i < yangHui.length; i++) {
            for (int j = 0; j < yangHui[i].length; j++) {
                System.out.print(yangHui[i][j] + " ");
            }
            System.out.println();
        }
    }
}

回形数

public class $_01_回形数 {
    public static void main(String[] args) {
        int n = 7;
        int[][] arr = new int[n][n];
 
        int count = 0; // 要显示的数据
        int maxX = n - 1; // x轴的最大下标
        int maxY = n - 1; // Y轴的最大下标
        int minX = 0; // x轴的最小下标
        int minY = 0; // Y轴的最小下标
        while (minX <= maxX) {
            for (int x = minX; x <= maxX; x++) {
                arr[minY][x] = ++count;
            }
            minY++;
            for (int y = minY; y <= maxY; y++) {
                arr[y][maxX] = ++count;
            }
            maxX--;
            for (int x = maxX; x >= minX; x--) {
                arr[maxY][x] = ++count;
            }
            maxY--;
            for (int y = maxY; y >= minY; y--) {
                arr[y][minX] = ++count;
            }
            minX++;
        }
 
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length; j++) {
                String space = (arr[i][j] + "").length() == 1 ? "0" : "";
                System.out.print(space + arr[i][j] + " ");
            }
            System.out.println();
        }
    }
}

数组最大值、最小值、平均数、总和

public class $_03_数值 {
    public static void main(String[] args) {
        /**
         * 定义一个int型的一维数组,包含10个元素,分别赋一些随机整数,
         * 然后求出所有元素的最大值,最小值,和值,平均值,并输出出来。
         * 要求:所有随机数都是两位数。
         * 提示;
         * [0,1) * 90 ?[0,90) + 10 ? [10,100) ?[10,99]
         * (int)(Math.random() * 90 + 10)
         */
        int[] arr = new int[10];
 
        for (int i = 0; i < 10; i++) {
            arr[i] = (int) (Math.random() * 99 + 10);
        }
        // 最大值
        int maxValue = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (maxValue < arr[i]) {
                maxValue = arr[i];
            }
        }
        System.out.println("最大值为:" + maxValue);
        // 最小值
        int minValue = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (minValue > arr[i]) {
                minValue = arr[i];
            }
        }
        System.out.println("最小值为:" + minValue);
        // 和值
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        System.out.println("和值:" + sum);
        // 平均值
        System.out.println("平均值为:" + (sum / arr.length));
        
    }
}

数组复制

public class $_04_数组复制 {
    public static void main(String[] args) {
   String[] arr = new String[]{"JJ","DD","MM","GG","NN"};
 
        // 数组的复制
        String[] arr1 = new String[arr.length];
        for (int i = 0; i < arr1.length; i++) {
            arr1[i] = arr[i];
        }
    }
}

数组反转

public class $_05_数组反转 {
    public static void main(String[] args) {
        String[] arr = new String[]{"JJ", "DD", "MM", "GG", "NN"};
 

        // 数组的反转(这里不是倒着遍历,交换位置)
        // 方式一:
        for (int i = 0; i < arr.length / 2; i++) {
            String temp = arr[i];
            arr[i] = arr[arr.length - 1 - i];
            arr[arr.length - 1 - i] = temp;
        }
        System.out.println(Arrays.toString(arr));
    	// 方式二:
        for (int i = 0, j = arr.length - 1; i < j; i++, j--) {
            String temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
        System.out.println(Arrays.toString(arr));
    }
}

线性查找/搜索

public class $_05_数组查找 {
    public static void main(String[] args) {
        String[] arr = new String[]{"JJ", "DD", "MM", "GG", "NN"};
 
        String search = "BB";
        // 线性查找
        boolean flag = true;
        for (int i = 0; i < arr.length; i++) {
            if (search.equals(arr[i])) {
                System.out.println("找到了指定的元素,位置位:"+i);
                flag = false;
                break;
            }
        }
        if (flag) {
            System.out.println("没有找到");
        }
    }
}

二分法查找

比线性查找快;

前提:要查找的数组必须有序

public class $_05_数组查找 {
    public static void main(String[] args) {
        // 二分法查找
        int[] arr = new int[]{-99, -54, -2, 0, 2, 33, 43, 256, 999};
        boolean isFlag = true;
        int number = 43;
        int head = 0; // 初始的首索引
        int end = arr.length - 1; // 初始的末索引
        while (head <= end) {
            int middle = (head + end) / 2;
            if (number == arr[middle]) {
                System.out.println("找到元素,位置位 :" + middle);
                isFlag = false;
                break;
            } else if (arr[middle] > number) {
                end = middle - 1;
            } else {
                head = middle + 1;
            }
        }
        if (isFlag) {
            System.out.println("没有找到");
        }
    }
}

数组元素的排序算法

为什么用排序?

通常来说,排序的目的就是快速查找

衡量排序算法的优劣

  • 1.时间复杂度:分析关键字的比较次数和记录的移动次数
  • 2.空间复杂度:分析排序算法中需要多少辅助内存
  • 3.稳定性:若了两个记录A和B的关键字值相等,但排序A,B的先后次序保持不变,称为稳定的。

(关键字,更常用于对象的某个属性,比如,商品一开始按销量从高到低排了,但是有些商品的价格相同,我们再按价格从低到高排,而这时候,希望价格相同的,销量还是高的在上面。就需要一个稳定的算法。)

十大内部排序算法(内存) 外部排序(磁盘)

1.选择排序

  • 直接选择排序
  • 堆排序

2.交换排序

  • 冒泡排序
排序思想:
1. 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。
public class $_07_BubbleSort {
    public static void main(String[] args) {
        // 冒泡排序
        int[] arr = new int[]{43, 32, 76, -98, 0, 64, 33, -21, 32, 99};
        System.out.println(Arrays.toString(arr));
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (arr[j] > arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(arr));
    }
}
  • 快速排序
分治法的思想,20世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种。快速排序的时间复杂度为O(nlog(n))。
排序思想:
1. 从数列中挑出一个元素,称为"基准"(pivot),
2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
4. 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
/**
* 通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,
* 则分别对这两部分继续进行排序,直到整个序列有序。
*/
public class QuickSort {
    private static void swap(int[] data, int i, int j) {
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }
    private static void subSort(int[] data, int start, int end) {
        if (start < end) {
            int base = data[start];
            int low = start;
            int high = end + 1;
            while (true) {
                while (low < end && data[++low] - base <= 0)
                    ;
                while (high > start && data[--high] - base >= 0)
                    ;
                if (low < high) {
                    swap(data, low, high);
                } else {
                    break;
                }
            }
            swap(data, start, high);
            
            subSort(data, start, high - 1);//递归调用
            subSort(data, high + 1, end);
        }
    }
    public static void quickSort(int[] data){
        subSort(data,0,data.length-1);
    }
    
    public static void main(String[] args) {
        int[] data = { 9, -16, 30, 23, -30, -49, 25, 21, 30 };
        System.out.println("排序之前:\n" + java.util.Arrays.toString(data));
        quickSort(data);
        System.out.println("排序之后:\n" + java.util.Arrays.toString(data));
    }
}

3.插入排序

  • 直接插入排序
  • 折半插入排序
  • Shell(希尔)排序

4.归并排序

5.桶式排序

6.基数排序

各种内部排序方法性能比较

1.从平均时间而言快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序

Image

2.从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于Shell排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序。

3.从稳定性看:直接插入排序、冒泡排序和归并排序时稳定的;而直接选择排序、快速排序、 Shell排序和堆排序是不稳定排序

4.从待排序的记录数n的大小看,n较小时,宜采用简单排序;而n较大时宜采用改进排序

选择:
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
   当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。

Arrays操作数组

为什么用Arrays

之前学了哪些复制,反转,二分查找,排序等,实际上是要你记住实现的思想,实际开发中我们使用Arrays工具类操作数组。

怎么用

Image

public class $_03_工具类Arrays {
 
    public static void main(String[] args) {
        // 1 boolean equals(int[] a,int[] b) 判断两个数组是否相等。
        int[] arr1 = new int[]{1,2,3,4};
        int[] arr2 = new int[]{1,3,2,4};
        boolean isEquals = Arrays.equals(arr1, arr2);
        System.out.println("isEquals = " + isEquals);
 
        // 2 String toString(int[] a) 输出数组信息。
        String str = Arrays.toString(arr1);
        System.out.println("str = " + str);
 
        // 3 void fill(int[] a,int val) 将指定值填充到数组之中。
        Arrays.fill(arr1,10);
        System.out.println(Arrays.toString(arr1));
 
        // 4 void sort(int[] a) 对数组进行排序。
        Arrays.sort(arr2);
        System.out.println(Arrays.toString(arr2));
 
        // 5 int binarySearch(int[] a,int key) 对排序后的数组进行二分法检索指定的值。
        int[] arr = new int[]{-99, -54, -2, 0, 2, 33, 43, 256, 999};
        int index = Arrays.binarySearch(arr, 210);
        System.out.println("index = " + index); // 没找到,返回负数
    }
 
}

面向对象

类和对象

面向对象和面向过程编程思想

编程思想其实就是编程思路,我们开发中2种经典的编程思想就是面向过程编程思想和面向对象编程思想.

  • 面向过程编程思想强调的是过程,必须清楚每一个步骤,然后按照步骤一步一步去实现
  • 面向对象编程思想强调的是对象, 通过调用对象的行为来实现功能,而不是自己一步一步的去操作实现。

举例对比2种编程思想

  • 洗衣服:

    • 面向过程:把衣服脱下来-->找一个盆-->放点洗衣粉-->加点水-->浸泡10分钟-->揉一揉-->清洗衣服-->拧干-->晾起来
    • 面向对象: 把衣服脱下来-->给女朋友去洗
  • 吃饭

    • 面向过程: 买菜--->洗菜--->切菜---->炒菜--->吃
    • 面向对象: 找个饭店-->20块钱

java程序上的区别:

// 需求:打印数组中所有的元素,打印格式为: [元素1,元素2,元素3,元素,...,元素n]
public class Test {
  public static void main(String[] args) {
      /*
          面向过程编程思想
              - 强调的是过程,必须清楚每一个步骤,然后按照步骤一步一步去实现

          面向对象编程思想
              - 强调的是对象, 通过调用对象的行为来实现功能,而不是自己一步一步的去操作实现。
       */
      // 需求:打印数组中所有的元素,打印格式为: [元素1,元素2,元素3,元素,...,元素n]
      // 1 定义一个数组,并且初始化数组中的元素
      int[] arr = {10, 20, 30, 40, 50};

      // 面向过程:
      // 2.循环遍历数组
      for (int i = 0; i < arr.length; i++) {
          // 3.在循环中,获取遍历出来的元素
          int e = arr[i];
          // 4.判断该元素:
          if (i == 0) {
              // 4.1 如果该元素是第一个元素,打印格式: [ + 元素 + 逗号空格  不换行
              System.out.print("[" + e + ", ");
          } else if (i == arr.length - 1) {
              // 4.2 如果该元素是最后一个元素,打印格式: 元素 + ]
              System.out.println(e + "]");
          } else {
              // 4.3 如果该元素是中间元素,打印格式为: 元素 + 逗号空格   不换行
              System.out.print(e + ", ");
          }
      }

      System.out.println("==================================");

      // 面向对象:
      // jdk的api中有一个Arrays类toString()方法,可以帮助我们按照这种格式打印数组中的所有元素
      System.out.println(Arrays.toString(arr));
  }
}

类的概述

类的概述

  • 类是用来描述一类具有共同属性和行为事物的统称。所以其实类在客观世界里是不存在的,是抽象的,只是用来描述数据信息的。
  • 手机类---描述手机
  • 人类----描述人

类的组成

  • 属性:该类事物的状态信息,在类中通过成员变量来体现(类中方法外的变量)
  • 行为:该类事物有什么功能,在类中通过成员方法来体现(和前面的方法相比去掉static关键字即可)

举例

  • 手机类
    • 属性:品牌、价格...。
    • 行为:打电话、发短信...。
  • 人类:
    • 属性: 姓名,年龄,性别....
    • 行为:吃饭,睡觉,.....

对象的概述

对象的概念

  • 对象是类的一个实例(并不是你的女朋友哈),具体存在的,看得见摸得着的,并且具备该类事物的属性和行为
    • 对象的属性:对象的属性具有特定的值
    • 对象的行为:对象可以操作的行为

举例

  • 对象: 你手上拿的这台手机
    • 属性:华为、1999。 对象的属性具体的值,类中的属性没有具体的值
    • 行为:使用打电话功能,使用发短信功能。对象可以使用行为

类和对象的关系

  • 类是对一类具有共同属性和行为的事物的统称,是抽象的

  • 对象是一类事物的具体实例,看得见,摸的着的,真实存在的实体,是具体的

  • 类是对象的抽象,对象是类的实体

    1584029135069

类的定义

public class 类名 {// 定义一个类
	// 类里面:属性(成员变量),行为(成员方法)
    // 定义成员变量
    数据类型 变量名1;
    数据类型 变量名2;
    ...
        
    // 定义成员方法
    方法;  去掉static
}

举例

// 定义一个手机类,类名为(Phone),类的属性有:品牌(brand),价格(price),类的行为:打电话(call),发短信(sendMessage)
public class Phone {
    //属性(成员变量): 数据类型 变量名;
    /**
     * 品牌
     */
    String brand;
    /**
     * 价格
     */
    double price;

    //行为(成员方法): 去掉static

    /**
     * 打电话的功能
     * @param phoneNum 电话号码
     */
    public void call(String phoneNum){
        System.out.println("正则给"+phoneNum+"打电话...");
    }

    /**
     * 发短信的功能
     * @param phoneNum 电话号码
     * @param message  短信内容
     */
    public void sendMessage(String phoneNum,String message){
        System.out.println("正在给"+phoneNum+"发送短信,短信内容是:"+message);
    }
}

对象的创建和使用

创建对象的格式:

  • 类名 对象名 = new 类名();
  • 类其实就是对象的数据类型,类是引用数据类型
  • 例: Phone p1 = new Phone (); 创建了一个手机对象(Phone类的对象)

对象的使用

  • 访问成员变量
    • 获取成员变量的值: 对象名.成员变量名
    • 给成员变量赋值: 对象名.成员变量名=值;
  • 访问成员方法
    • 对象名.成员方法();

案例演示

public class Phone {
    //属性(成员变量): 数据类型 变量名;
    /**
     * 品牌
     */
    String brand;
    /**
     * 价格
     */
    double price;

    //行为(成员方法): 去掉static

    /**
     * 打电话的功能
     * @param phoneNum 电话号码
     */
    public void call(String phoneNum){
        System.out.println("正在给"+phoneNum+"打电话...");
    }

    /**
     * 发短信的功能
     * @param phoneNum 电话号码
     * @param message  短信内容
     */
    public void sendMessage(String phoneNum,String message){
        System.out.println("正在给"+phoneNum+"发送短信,短信内容是:"+message);
    }

    // 为了演示有返回值的方法调用
    public int show(String str){
        System.out.println("有参数有返回值的方法:"+str);
        return 100;
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            对象的创建和使用:
                对象的创建:
                    对象创建格式: 类名 对象名 = new 类名();
                    结论: 类其实也是一种数据类型,是引用数据类型
                对象的使用:
                    访问成员变量:
                        给成员变量赋值:  对象名.成员变量名 = 值;
                        获取成员变量的值: 对象名.成员变量名

                    访问成员方法:
                        调用方法: 有返回值的方法,无返回值的方法
                        无返回值的方法:
                               直接调用: 对象名.方法名(实参);
                        有返回值的方法:
                               直接调用: 对象名.方法名(实参);
                               赋值调用: 数据类型 变量名 = 对象名.方法名(实参);
                               输出调用: System.out.println(对象名.方法名(实参));
         */
        // 创建Phone类的对象
        Phone p1 = new Phone();
        // 给p1对象的brand成员变量赋值
        p1.brand = "华为";
        // 给p1对象的price成员变量赋值
        p1.price = 999.8;

        // 获取p1对象的brand成员变量的值
        System.out.println(p1.brand);
        // 获取p1对象的price成员变量的值
        System.out.println(p1.price);

        // 无返回值的成员方法
        // 使用p1对象调用call方法
        p1.call("10086");
        // 使用p1对象调用sendMessage方法
        p1.sendMessage("10086","请问一下联通的客服电话号码是多少?");

        System.out.println("==============================");
        // 有返回值的方法
        // 直接调用
        p1.show("张三");

        // 赋值调用
        int res = p1.show("李四");// 100
        System.out.println("res:"+res);// 100

        // 输出调用
       System.out.println(p1.show("java"));// 100

        /*
        之前访问变量
        int num;
        num = 10;
        System.out.println(num);*/
        /*
        之前访问访问
            方法分类: 无参数无返回值,有参数无返回值,有参数有返回值,无参数有返回值
            调用方法: 有返回值的方法,无返回值的方法
                无返回值的方法:
                       直接调用: 方法名(实参);
                有返回值的方法:
                       直接调用: 方法名(实参);
                       赋值调用: 数据类型 变量名 = 方法名(实参);
                       输出调用: System.out.println(方法名(实参));
        */
    }
}

学生对象-练习

需求

  • 首先定义一个学生类,然后定义一个学生测试类,在学生测试类中通过对象完成成员变量和成员方法的使用

分析

  • 定义学生类
    • 成员变量:姓名,年龄…
    • 成员方法:学习,做作业…
  • 测试类
    • 创建main方法,在main 方法中创建学生对象
    • 使用学生对象访问成员变量和访问成员方法

实现

public class Student {
    // 成员变量: 属性
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
    int age;

    // 成员方法: 行为
    /**
     * 学习的功能
     */
    public void study(){
        System.out.println("学生正在学习Java...");
    }

    /**
     * 做作业的功能
     */
    public void doHomeWork(){
        System.out.println("学生正在做作业敲代码...");
    }
}


public class Test {
    public static void main(String[] args) {
        // 需求:首先定义一个学生类,然后定义一个学生测试类,在学生测试类中通过对象完成成员变量和成员方法的使用
        // 创建学生对象
        Student stu = new Student();

        // 访问成员变量
        stu.name = "冰冰";
        stu.age = 18;
        System.out.println(stu.name+","+stu.age);// 冰冰,18

        // 访问成员方法
        stu.study();
        stu.doHomeWork();

    }
}

成员变量默认值

public class Student {
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
    int age;
    /**
     * 分数
     */
    double score;
    char c;
}


public class Test {
    public static void main(String[] args) {
        // 变量的要使用一定要赋值
        //int num;// 局部变量:定义在方法中的变量
        //System.out.println(num);// 编译报错,局部变量没有默认值

        /*
            成员变量的默认值:
                整数类型: 默认值是0
                小数类型: 默认值是0.0
                布尔类型: 默认值是false
                字符类型: 默认值是不可见字符 '\u0000'
                引用类型: 默认值是null
         */
        // 创建Student对象
        Student stu = new Student();
        // 访问成员变量
        System.out.println(stu.name);// null
        System.out.println(stu.age);// 0
        System.out.println(stu.score);// 0.0
        System.out.println("="+stu.c+"=");
    }
}

单个对象内存图

public class Student {
    // 成员变量: 属性
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
    int age;

    // 成员方法: 行为
    /**
     * 学习的功能
     */
    public void study(){
        System.out.println("学生正在学习Java...");
    }

    /**
     * 做作业的功能
     */
    public void doHomeWork(){
        System.out.println("学生正在做作业敲代码...");
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建Student对象
        Student stu = new Student();
        System.out.println(stu);// 十六进制数地址值

        // 访问成员变量
        stu.name = "冰冰";
        stu.age = 18;
        System.out.println(stu.name+","+stu.age);

        // 访问成员方法
        stu.study();
        stu.doHomeWork();
    }
}

image-20200905121918328

多个对象内存图

public class Student {
    // 成员变量: 属性
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
    int age;

    // 成员方法: 行为
    /**
     * 学习的功能
     */
    public void study(){
        System.out.println("学生正在学习Java...");
    }

    /**
     * 做作业的功能
     */
    public void doHomeWork(){
        System.out.println("学生正在做作业敲代码...");
    }
}


public class Test {
    public static void main(String[] args) {
        // 创建Student对象  shift+f6+fn 批量修改名称
        Student stu1 = new Student();
        System.out.println(stu1);// 十六进制数地址值

        // 访问成员变量
        stu1.name = "冰冰";
        stu1.age = 18;
        System.out.println(stu1.name+","+stu1.age);// 冰冰,18

        // 访问成员方法
        stu1.study();
        stu1.doHomeWork();

        System.out.println("==========================");

        Student stu2 = new Student();

        System.out.println(stu2.name+","+stu2.age);// null,0
        stu2.study();

    }
}

绘制内存图

image-20200905123805520

注意:

  • 多个对象在堆内存中,都有不同的内存划分,成员变量存储在各自对象的内存区域中,成员方法多个对象共用的一份
  • 凡是new就会重新在堆区开辟一块新空间
  • 对象和对象之间的关系是相互独立的

多个变量指向相同对象内存图

查看程序案例

public class Student {
    // 成员变量: 属性
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
    int age;

    // 成员方法: 行为
    /**
     * 学习的功能
     */
    public void study(){
        System.out.println("学生正在学习Java...");
    }

    /**
     * 做作业的功能
     */
    public void doHomeWork(){
        System.out.println("学生正在做作业敲代码...");
    }
}
public class Test {
    public static void main(String[] args) {
        // 创建Student对象
        Student stu1 = new Student();

        // 访问学生对象的成员变量
        stu1.name = "冰冰";
        stu1.age = 18;
        System.out.println(stu1.name + "," + stu1.age);// 冰冰,18

        // 访问学生对象的成员方法
        stu1.study();

        System.out.println("============================");
        // 定义一个Student类型的变量,并把之前创建的学生对象赋值给该变量
        Student stu2 = stu1;

        // 再使用新的变量访问成员变量
        System.out.println(stu2.name + "," + stu2.age);// 冰冰,18
        // 再使用新的变量访问成员方法
        stu2.study();
    }
}

绘制内存图

image-20200905144926634

注意点:

  • 当多个对象的引用指向同一个内存空间(变量所记录的地址值是一样的)
  • 只要有任何一个对象修改了内存中的数据,随后,无论使用哪一个对象进行数据获取,都是修改后的数据。
  • 引用类型传递的是地址值

成员变量和局部变量的区别

1582716017361

  • 类中位置不同:成员变量(类中方法外)局部变量(方法内部或方法声明上)
  • 内存中位置不同:成员变量(堆内存)局部变量(栈内存)
  • 生命周期不同:成员变量(随着对象的存在而存在,随着对象的消失而消失)局部变量(随着方法的调用而存在,随着方法的调用完毕而消失)
  • 初始化值不同:成员变量(有默认初始化值)局部变量(没有默认初始化值,必须先定义,赋值才能使用)
public class Car {
    String color;// 成员变量

    // 成员方法
    public void drive(){
        int speed = 80;
        System.out.println("汽车正在以"+speed+"迈的速度行驶...");
    }
}


public class Test {

    /*
        成员变量和局部变量的区别:
            定义的位置不同: 成员变量定义在类中方法外,局部变量定义在方法中
            在内存中的位置不同: 成员变量是在堆区,局部变量是在栈区
            生命周期不同:
                成员变量是随着对象的创建而存在,随着对象的销毁而销毁
                局部变量是随着方法的调用而存在,随着方法调用完毕而销毁
            默认值不同:
                成员变量有默认值
                局部变量没有默认值,不赋值不能直接使用
     */
    public static void main(String[] args) {
         // 创建Car对象
        Car car = new Car();
        // 调用方法
        car.drive();
    }
}

image-20200806121538241

封装

private关键字

private的含义

  • 概述: private是一个权限修饰符,代表最小权限。
  • 特点:
    • 可以修饰成员变量和成员方法。
    • 被private修饰后的成员变量和成员方法,只在本类中才能访问。

private的使用格式

// private关键字修饰成员变量
private 数据类型 变量名 ;

// private关键字修饰成员方法
private 返回值类型 方法名(参数列表){
    代码
}

案例

public class Student {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;

    
    private void study(){
        System.out.println("正在学习java");
    }

    public void show(){
        // 只能在本类中访问
        System.out.println(name+","+age);
    }
}


public class Test {
    public static void main(String[] args) {
        /*
            private关键字:
                概述:是一个权限修饰符,最小的权限
                特点:
                    1.private可以修饰成员变量和成员方法
                    2.被private修饰后的成员变量和成员方法,只在本类中才能访问。
                使用:
                    修饰成员变量格式:  private 数据类型 变量名;
                    修饰成员方法格式:  private 返回值类型 方法名(形参列表){方法体}
         */
        // 创建Student类对象
        Student stu1 = new Student();

        // 直接访问stu1的成员变量
        //stu1.name = "冰冰";// 编译报错,因为没有访问权限
        //stu1.age = 18;// 编译报错,因为没有访问权限

        // 直接访问stu1的成员方法
        //stu1.study();// 编译报错,因为没有访问权限

    }
}

为什么要对属性进行封装

public class Student {
    /**
     * 姓名
     */
    String name;
    /**
     * 年龄
     */
     int age;
}

public class Test {
    public static void main(String[] args) {
        /*
            为什么要对属性进行封装:
                通过对象名直接访问成员变量的方式来对属性赋值,会存在数据安全隐患,应该怎么解决呢?
                解决方式: 不让外界直接访问成员变量(也就是要对属性进行封装\隐藏)
            对成员变量隐藏的步骤:
                1.使用private关键字修饰成员变量
                2.提供公共的访问方法:
                    给成员变量赋值的公共方法(set方法)
                    获取成员变量值的公共方法(get方法)
         */
        // 创建Student对象
        Student stu1 = new Student();

        // 访问成员变量
        stu1.name = "冰冰";
        // 通过对象名直接访问成员变量的方式来对属性赋值,会存在数据安全隐患,应该怎么解决呢?
        stu1.age = -18;
        System.out.println(stu1.name + "," + stu1.age);// 冰冰,-18

    }
}

set和get方法

set和get方法的介绍

  • 由于属性使用了private关键字修饰,在其他类中无法直接访问,所以得提供公共的访问方法,我们把这张方法叫做set和get方法

    • get方法: 提供“get变量名()”方法,用于获取成员变量的值,方法用public修饰
    • set方法: 提供“set变量名(参数)”方法,用于设置成员变量的值,方法用public修饰

set和get方法的书写

public class Student {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
     private int age;

     // 提供给成员变量赋值的方法-set方法
    public void setName(String s){
        name = s;
    }

    public void setAge(int a){
        if (a < 0 || a > 150){
            age = -1;
            System.out.println("您的数据不合法!");
        }else{
            age = a;
        }
    }
     // 提供获取成员变量值的方法-get方法
    public String getName(){
        return name;
    }

    public int getAge(){
        return age;
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            通过对象名直接访问成员变量的方式来对属性赋值,会存在数据安全隐患,应该怎么解决呢?
            解决方式: 使用private修饰,并提供公共的访问方法
         */
        // 创建Student对象
        Student stu1 = new Student();

        // 访问成员变量
        // 隐藏属性后的方式
        stu1.setName("冰冰");
        stu1.setAge(-18);
        System.out.println(stu1.getName()+","+stu1.getAge());// 冰冰,-1

        // 没有隐藏属性之前的方式
        //stu1.name = "冰冰";
        //stu1.age = -18;
        //System.out.println(stu1.name + "," + stu1.age);// 冰冰,-18
    }
}

this关键字

问题

我们发现 setXxx 方法中的形参名字并不符合见名知意的规定,那么如果修改与成员变量名一致,是否就见名知意了呢?代码如下:

public class Student {
  private String name;
  private int age;

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

  public void setAge(int age) {
    age = age;
  }
}

经过修改和测试,我们发现新的问题,成员变量赋值失败了。也就是说,在修改了setXxx() 的形参变量名后,方法并没有给成员变量赋值!这是由于形参变量名与成员变量名重名,导致成员变量名被隐藏,方法中的变量名,无法访问到成员变量,从而赋值失败。所以,我们只能使用this关键字,来解决这个重名问题。

this的含义和使用

  • this含义: this代表当前调用方法的引用,哪个对象调用this所在的方法,this就代表哪一个对象

  • this关键字其主要作用是区分同名的局部变量和成员变量

    • 方法的形参如果与成员变量同名,不带this修饰的变量指的是形参,而不是成员变量
    • 方法的形参没有与成员变量同名,不带this修饰的变量指的是成员变量
  • this的使用格式:

    this.成员变量名
    
  • 使用 this 修饰方法中的变量,解决成员变量被隐藏的问题,代码如下:

    public class Student {
        /**
         * 姓名
         */
        private String name;
        /**
         * 年龄
         */
         private int age;
    
         // 提供给成员变量赋值的方法-set方法
        public void setName(String name){
            this.name = name;
        }
    
        public void setAge(int age){
            if (age < 0 || age > 150){
                this.age = -1;
                System.out.println("您的数据不合法!");
            }else{
                this.age = age;
            }
        }
         // 提供获取成员变量值的方法-get方法
        public String getName(){
            return name;
        }
    
        public int getAge(){
            return age;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            /*
                问题1:set方法的形参名不能起到知名达意(不符合标识符命名规范)
                解决1:把形参名修改成符合命名规范
                问题2:set方法的形参名改为符合命名规范后,发现set方法无法给成员变量赋值
                解决2:使用this关键字来区别同名的成员变量和局部变量
                    格式: this.成员变量名
                    this表示谁: 哪个对象调用this所在的方法,this就表示哪个对象
    
               结论:
                    1.如果成员方法中有与成员变量同名的局部变量,那么就需要使用this关键字来区分
                    2.如果成员方法中没有与成员变量同名的局部变量,那么就不需要使用this关键字来区分(直接使用成员变量即可)
             */
            // 创建Student对象
            Student stu1 = new Student();
    
            // 访问成员变量
            // 隐藏属性后的方式
            stu1.setName("冰冰");
            stu1.setAge(-18);
            System.out.println(stu1.getName()+","+stu1.getAge());// 冰冰,-1
    
            Student stu2 = new Student();
            stu2.setName("空空");
    
        }
    }
    
    

    小贴士:方法中只有一个变量名时,默认也是使用 this 修饰,可以省略不写。

this内存原理

代码

public class Test {
    public static void main(String[] args) {
        /*
            问题1:set方法的形参名不能起到知名达意(不符合标识符命名规范)
            解决1:把形参名修改成符合命名规范
            问题2:set方法的形参名改为符合命名规范后,发现set方法无法给成员变量赋值
            解决2:使用this关键字来区别同名的成员变量和局部变量
                格式: this.成员变量名
                this表示谁: 哪个对象调用this所在的方法,this就表示哪个对象

           结论:
                1.如果成员方法中有与成员变量同名的局部变量,那么就需要使用this关键字来区分
                2.如果成员方法中没有与成员变量同名的局部变量,那么就不需要使用this关键字来区分(直接使用成员变量即可)
         */
        // 创建Student对象
        Student stu1 = new Student();

        // 访问成员变量
        // 隐藏属性后的方式
        stu1.setName("冰冰");
        stu1.setAge(-18);
        System.out.println(stu1.getName()+","+stu1.getAge());// 冰冰,-1

        Student stu2 = new Student();
        stu2.setName("空空");
        System.out.println(stu2.getName()+","+stu2.getAge());// 空空,0

    }
}

public class Student {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
     private int age;

     // 提供给成员变量赋值的方法-set方法
    public void setName(String name){
        this.name = name;
    }

    public void setAge(int age){
        if (age < 0 || age > 150){
            this.age = -1;
            System.out.println("您的数据不合法!");
        }else{
            this.age = age;
        }
    }
     // 提供获取成员变量值的方法-get方法
    public String getName(){
        return name;
    }

    public int getAge(){
        return age;
    }
}

image-20200905162906642

封装概述

封装概述

  • 是面向对象三大特征之一(封装,继承,多态)
  • 是面向对象编程语言对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界是无法直接操作的

封装原则

  • 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问
  • 例如:成员变量使用private修饰,提供对应的getXxx()/setXxx()方法

封装好处

  • 通过方法来控制成员变量的操作,提高了代码的安全性
  • 把代码用方法进行封装,提高了代码的复用性

构造方法

  • 概述

构造方法是一种特殊的方法,主要是完成对象的创建和对象数据的初始化

  • 格式

    // 空参构造方法
    修饰符 类名(){
        
    }
    
    // 有参构造方法
    修饰符 类名(参数列表){
    	// 方法体
    }
    
    
    
  • 特点:

    • 构造方法的写法上,方法名与它所在的类名相同
    • 构造方法没有返回值,所以不需要返回值类型,甚至不需要void
  • 示例代码:

public class Student {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;

    // 构造方法
    public Student(){
        System.out.println("空参方法");
    }

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

    public String getName(){
        return name;
    }

    public int getAge(){
        return age;
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            构造方法:
                概述:构造方法是一个特殊的方法,主要用来创建对象并给属性赋值.
                定义:
                    无参构造方法:
                        权限修饰符 类名(){
                        }
                    有参构造方法:
                        权限修饰符 类名(形参列表){
                            给属性赋值
                        }
                特点:
                    1.构造方法没有返回值类型,连void不能写
                    2.构造方法的名字就是类名
                    3.通过new来调用构造方法
                使用: 通过new来调用
         */
        // 通过调用空参构造方法创建对象
        Student stu1 = new Student();
        System.out.println(stu1.getName()+","+stu1.getAge());// null,0

        // 通过调用有参构造方法创建对象
        Student stu2 = new Student("冰冰",18);
        System.out.println(stu2.getName()+","+stu2.getAge());// 冰冰,18

    }
}

注意事项

  • 构造方法的创建

    • 如果没有定义构造方法,系统将给出一个默认的无参数构造方法
    • 如果定义了构造方法,系统将不再提供默认的构造方法
  • 构造方法可以重载,既可以定义参数,也可以不定义参数。

  • 示例代码

public class Student {
    /**
     * 姓名
     */
    private String name;
    /**
     * age
     */
    private int age;

    // 空参构造方法
    public Student(){

    }
    // 有参构造方法(满参构造方法)
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }

    // 有参构造方法
    public Student(String name){
        this.name = name;
    }

    // 有参构造方法
    public Student(int age){
        this.age = age;
    }

    public void setAge(int age){
        this.age = age;
    }

    public int getAge(){
        return age;
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            构造方法的注意事项:
                1.构造方法没有返回值,连void都不能写
                2.构造方法名和类名一致
                3.如果一个类没有定义构造方法,系统会自动生成一个空参构造方法
                4.如果一个类定义了构造方法,系统就不会自动生成一个空参构造方法
                5.构造方法可以重载
                6.构造方法只能给属性赋值一次,而set方法可以给属性赋值无数次
                  因为调用构造方法,就会创建一个新的对象

         */
         //调用空参构造方法创建对象
        Student stu1 = new Student();

        // 通过有参构造方法创建对象
        Student stu2 = new Student("冰冰",18);
        Student stu3 = new Student("冰冰",18);

        System.out.println(stu2.getAge());// 18
        // 通过set方法给属性赋值
        stu2.setAge(19);
        System.out.println(stu2.getAge());// 19
        stu2.setAge(20);
        System.out.println(stu2.getAge());// 20

    }
}

小结

构造方法的注意事项:
    - 构造方法的创建
      - 如果没有定义构造方法,系统将给出一个默认的无参数构造方法
      - 如果定义了构造方法,系统将不再提供默认的构造方法
	- 构造方法只能给属性赋值一次,不能重复赋值,可以谁有set方法给属性重复赋值
    - 构造方法可以重载,既可以定义参数,也可以不定义参数。
    - 定义构造方法的时候,不要写返回值,连void都不能有
    - 定义构造方法的时候,构造方法名和类名一定要一致

标准类制作

标准类的组成

JavaBean 是 Java语言编写类的一种标准规范。符合JavaBean 的类,要求类必须是公共的,属性使用private修饰,并且具有无参数的构造方法,提供用来操作成员变量的setget 方法。

public class ClassName{
  //成员变量  private
  //构造方法
  //无参构造方法【必须】
  //满参构造方法【建议】
  //getXxx()
  //setXxx()
  //成员方法	
}

案例演示

  • 需求:定义标准学生类,要求分别使用空参和有参构造方法创建对象,空参创建的对象通过setXxx赋值,有参创建的对象直接赋值,并通过show方法展示数据。
  • 示例代码:
public class Student {
    // 成员变量--private
    /**
     * 姓名
     */
    private String name;
    /**
     * 年龄
     */
    private int age;

    // 空参构造方法  alt+insert--->Constructor
    public Student() {
    }

    // 满参构造方法(建议)
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // set\get方法 alt+insert---> setter and getter
    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;
    }

    // 成员方法
    public void show(){
        System.out.println(name+","+age);
    }

}

API

什么是API

​ API (Application Programming Interface) :应用程序编程接口。Java API是一本程序员的字典 ,是JDK中提供给我们使用的类的说明文档。这些类将底层的代码实现封装了起来,我们不需要关心这些类是如何实现的,只需要学习这些类如何使用即可。所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。

  • API其实就是jdk中核心类库的说明文档
  • 对于jdk中的核心类库只需要知道如何使用,无须关心他是如何实现的

使用步骤

  1. 打开API帮助文档。
  2. 点击显示,找到索引,看到输入框。
  3. 你要找谁?在输入框里输入,然后回车。
  4. 看包。java.lang下的类不需要导包,其他需要。
  5. 看类的解释和说明。
  6. 看构造方法。
  7. 看成员方法。

演示API的使用

  • 打开帮助文档

  • 找到索引选项卡中的输入框

  • 在输入框中输入Random

  • 看类在哪个包下

  • 看类的描述

  • 看构造方法

  • 看成员方法

public class Test {
    public static void main(String[] args) {
        /*
            api的使用步骤:
                1.打开api文档
                2.点击显示
                3.点击索引,在输入框中,输入要查找的类\接口
                4.查看类的包  如果在java.lang包就不需要导包,其余都需要导包
                5.查看类的解释说明
                6.查看类的构造方法
                7.查看类的成员方法

            举例: Scanner类
                1.查看类的包         java.util  导包
                2.查看类的解释说明    是一个文本扫描器,可以扫描基本类型的数据和字符串
                3.查看类的构造方法    Scanner(InputStream source)
                4.查看类的成员方法
                     byte nextByte()
                     short nextShort()
                     short nextInt()
                     Long nextLong()
                    boolean nextBoolean()
                     double nextDouble()
                    float nextFloat()

                     String nextLine()  可以获取一行字符串   空格,回车,tab键都可以获取
                     String next()      可以获取单个字符串   空格,回车,tab键都不可以获取

         */
        Scanner sc = new Scanner(System.in);
        /*System.out.println("请输入一个整数:");
        int num = sc.nextInt();
        System.out.println(num);*/

       /* System.out.println("请输入一个小数:");
        double numD = sc.nextDouble();
        System.out.println(numD);*/

        //System.out.println("请输入一个字符串:");
       /* String str = sc.nextLine();
        System.out.println(str);*/

        /*String str = sc.next();
        System.out.println(str);*/

        System.out.println("请输入年龄:");
        int age = sc.nextInt();
        System.out.println("年龄:"+age);// 18

        System.out.println("请输入姓名:");
        String name = sc.next();
        System.out.println("姓名:"+name);

        //String name = sc.nextLine();
        //System.out.println("姓名:"+name);
    }
}

对象的内存图

image-20200906090256217

注意点:

  • 只要是new对象就会在堆区开辟一块独立的空间
  • 只要调用方法,方法就会被加载进栈
  • 只要方法执行完毕,方法就会被弹栈

匿名对象

什么是匿名对象:就是指"没有名字"的对象。

有名字的对象:
    Student stu = new Student();
    stu.show();
    stu.study();
匿名对象:
	new Student();

特点:匿名对象只能使用一次

public class Test {
    public static void main(String[] args) {
        /*
            匿名对象:
                概述:没有名字的对象
                特点:匿名对象只能使用一次
                使用场景:当某个类的对象只需要使用一次的时候,就可以使用该类的匿名对象
                        例如:方法的参数,方法的返回值
         */
        // 创建对象
        Student stu1 = new Student("热巴",18);// 有名字的对象
        stu1.show();
        stu1.show();

        System.out.println("==================================");
        //匿名对象
        new Student("热巴",18).show();// 没有名字的对象
        new Student("热巴",18).show();// 没有名字的对象

        System.out.println("==================================");
        // 调用method1方法
        Student stu2 = new Student("热巴",18);// 0x11901
        method1(stu2);// 有名字的对象传参
        method1(new Student("热巴",18));// 匿名对象的方式传参数

        System.out.println("==================================");
        Student stu3 = method2();// 0x11908
        stu3.show();// 丽颖,18

    }

    public static void method1(Student stu){// 0x11901
        stu.show();
    }


    public static Student method2(){
        //Student stu = new Student("丽颖",18);// 0x11908
        //return stu;// 0x11908

        return new Student("丽颖",18);
    }


}

继承

继承概述

为什么要有继承

现实生活中,为什么要有继承?

1574219808378

程序中为什么要有继承?

1574220305488

继承的含义

继承:在java中指的是“一个类”可以“继承自”“另一个类”。 "被继承的类"叫做: 父类/超类/基类,"继承其他类的类"叫做:子类。继承后,“子类”中就“拥有”了“父类”中所有的成员(成员变量、成员方法)。 “子类就不需要再定义了”。

继承的好处

  1. 提高代码的复用性(减少代码冗余,相同代码重复利用)。
  2. 使类与类之间产生了关系。

继承的格式

通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:

class 父类 {
	...
}

class 子类 extends 父类 {
	...
}

需要注意:Java是单继承的,一个类只能继承一个直接父类,并且满足is-a的关系,例如:Dog is a Animal, Student is a Person

继承的演示

人类:
public class Person {
    // 成员变量
    String name;
    int age;
    
    // 功能方法
    public void eat(){
        System.out.println("吃东西...");
    }

    public void sleep(){
        System.out.println("睡觉...");
    }
}
老师类: extends 人类
public class Teacher extends Person {
	double salary;// 独有的属性
    public void teach(){}// 独有的方法
}
学生类: extends 人类
public class Student extends Person{

}
Dog:  extends 人类
public class Dog extends Person{// 语法上是可以的,但不符合现实逻辑(不符合is a的关系)

}   
测试:
public class Test {
    public static void main(String[] args) {
        Teacher t = new Teacher();
        System.out.println(t.name);
        System.out.println(t.age);
        t.eat();
        t.sleep();
    }
}

继承后成员访问规则

继承后构造方法的访问规则

  • 构造方法不能被继承

    class Fu {
        // 构造方法
        Fu(){}
        Fu(String name,int age){}
    }
    
    class Zi extends Fu{
    
    }
    
    public class Test {
        public static void main(String[] args) {
            /*
                构造方法的访问规则:父类的构造方法不能被子类继承
                私有成员的访问规则:
                非私有成员的访问规则:
             */
            //Zi zi = new Zi("张三",18);// 编译报错,因为没有继承
        }
    }
    
    

继承后私有成员的访问规则

  • 父类的“私有成员”可以被子类继承,但子类不能被直接访问。

    public class Fu{
        private int num = 100;//私有成员,只能在父类内部使用。
        private void method(){
            System.out.println("私有成员方法");
        }
    }
    public class Zi extends Fu{
    
    }
    public class Demo {
        public static void main(String[] args) {
            Zi z = new Zi();
    	    System.out.println(z.num);// 编译错误
            z.method();// 编译错误
        }
    }
    
    

继承后非私有成员的访问规则

  • 当通过“子类”访问非私有成员时,先在子类中找,如果找到就使用子类的,找不到就继续去“父类”中找。

    public class Fu{
        int money = 100;
        public void method(){
            System.out.println("Fu 类中的成员方法method");
        }
    }
    public class Zi extends Fu{
        int money = 1;
         public void method(){
            System.out.println("Zi 类中的成员方法method");
        }
    }
    public class Demo{
        public static void main(String[] args){
            Zi z = new Zi();
            System.out.println(z.money);//1
            z.method();// Zi 类中的成员方法method
        }
    }
    

方法重写

方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现

class Fu{
    public void method(){
        System.out.println("Fu method");
    }
}
class Zi extends Fu{

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

    public void show(){
        System.out.println("Zi show");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            方法重写:
                方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),
                          会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。
                注意事项:
                    1.一定要是父子类关系
                    2.子类中重写的方法返回值类型,方法名,参数列表一定要和父类一模一样
                    3.子类中重写的方法可以使用@Override注解进行标识,如果不是重写的方法使用@Override注解标识就会报错
                       建议开发中重写的方法使用@Override注解标识,这样可以提高代码的可读性
                    4.子类重写父类的方法的访问权限不能低于父类的访问权限
                        访问权限: public >  protected  >  默认(空)   >  private
         */
        Zi zi = new Zi();
        zi.method();
    }
}

注意事项

  • 方法重写是发生在子父类之间的关系。

  • 子类方法重写父类方法,返回值类型、方法名和参数列表都要一模一样。

  • 子类方法重写父类方法,必须要保证权限大于等于父类权限。

    • 访问权限从大到小: public protected (默认) private
  • 使用@Override注解,检验是否重写成功,重写注解校验!

    • 建议重写方法都加上这个注解,一方面可以提高代码的可读性,一方面可以防止重写出错!

使用场景

class Fu{
    public void sport(){
        System.out.println("Fu 运动的方式跑步");
    }

    public void run(){
        System.out.println("Fu 第1圈");
        System.out.println("Fu 第2圈");
        System.out.println("Fu 第3圈");
    }
}

class Zi extends Fu{
    // 子类方法的实现和父类方法的实现完全不同
    @Override
    public void sport() {
        System.out.println("Zi 运动的方式游泳");
    }

    // 子类方法的实现要保留父类方法的功能,但要在父类功能的基础之上额外增加功能
    @Override
    public void run() {
        // 让父类的方法执行=====复制父类的代码过来
        super.run();// 调用父类的方法

        // 额外增加的代码
        System.out.println("Zi 第4圈");
        System.out.println("Zi 第5圈");
        System.out.println("Zi 第6圈");
        System.out.println("Zi 第7圈");
        System.out.println("Zi 第8圈");
        System.out.println("Zi 第9圈");
        System.out.println("Zi 第10圈");
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            方法重写的使用场景:
                当父类的方法无法满足子类的需求的时候,子类就会去重写父类的方法
         */
        // 创建子类对象
        Zi zi = new Zi();
        // 调用运动的方法
        zi.sport();
        // 调用跑步的方法
        zi.run();


    }
}

this和super关键字

this和super关键字的介绍

  • this:存储的“当前对象”的引用;
    • this可以访问:本类的成员属性、成员方法、构造方法;
  • super:存储的“父类对象”的引用;
    • super可以访问:父类的成员属性、成员方法、构造方法;

this关键字的三种用法

  • this访问本类成员变量: this.成员变量

    public class Student{
        String name = "张三";
        public void show(){
            String name = "李四";
            System.out.println("name = " + name);// 李四
            System.out.println("name = " + this.name);// 张三
        }
    }
    
  • this访问本类成员方法: this.成员方法名();

    public class Student{
        public void show(){
            System.out.println("show方法...");
            this.eat();
        }
        public void eat(){
            System.out.println("eat方法...");
        }
    }
    
  • this访问本类构造方法: this()可以在本类的一个构造方法中,调用另一个构造方法

    public class Student{
        public Student(){
            System.out.println("空参构造方法...");
        }
    
        public Student(String name) {
            this();//当使用this()调用另一个构造方法时,此代码必须是此构造方法的第一句有效代码。
            System.out.println("有参构造方法...");
        }
    }
    public class Demo {
        public static void main(String[] args) {
            Student stu2 = new Student();
        }
    }
    

super关键字的三种用法

  • super访问父类的成员变量: super.父类成员变量名

    class Fu{
        int num = 100;
    }
    
    class Zi extends Fu{
        int num = 10;
    
        public void show(){
            int num = 1;
            System.out.println("局部变量num:"+num);// 1
            System.out.println("Zi 类中的num:"+this.num);// 10
            System.out.println("Fu 类中的num:"+super.num);// 100
    
          }
    }
    
  • super访问父类的成员方法: super.成员方法名();

    class Fu{
        public void method1(){
            System.out.println("Fu method1...");
        }
    }
    
    class Zi extends Fu{
        public void show(){
            // 访问父类的method1方法
            super.method1();
        }
    
        @Override
        public void method1(){
            super.method1();// 调用父类的method1方法
            System.out.println("Zi method1...");
        }
    }
    
  • super访问父类的构造方法: super()

    public class Fu{
        public Fu(){
            System.out.println("Fu 类的空参构造方法..");
        }
        public Fu(String name, int age) {
            System.out.println("Fu 类的有参构造方法..");
        }
    }
    public class Zi extends Fu{
        public Zi(){
            super();// 调用父类的空参构造方法
            System.out.println("Zi 类的空参构造方法..");
        }
        public Zi(String name,int age){
            super(name,age);// 调用父类的有参构造方法
             System.out.println("Zi 类的有参构造方法..");
        }
    }
    public class Demo {
        public static void main(String[] args) {
            Zi zi = new Zi();
            System.out.println("----------------------");
            Zi z2 = new Zi("刘德华", 17);
        }
    }
    
    

小结

  • this关键字的三种用法:
       this可以访问本类的成员变量: this.成员变量         一般用来区分同名的成员变量和局部变量
       this可以访问本类的成员访问: this.成员方法名(实参);   
       this可以访问本类的构造方法:
            空参构造: this();
            有参构造: this(实参);
                注意:
                     1.只能在本类的构造方法中使用this调用其他构造方法
                     2.在本类的构造方法中使用this调用其他构造方法,必须放在该构造方法的第一行,否则会报错
                     3.两个构造方法不能使用this同时相互调用
    
  • super关键字的三种用法:
         super可以访问父类的成员变量: super.成员变量         一般用来区分父子类中同名的成员变量
         super可以访问父类的成员方法: super.成员方法(实参);   一般用来在子类中访问父类的成员方法
         super可以访问父类的构造方法:
              空参构造: super();
              有参构造: super(实参);
               注意:
                     1.子类的构造方法默认会调用父类的空参构造方法
                     2.super访问父类的构造方法,可以用来初始化从父类继承过来的属性
                     3.在子类的构造方法中,使用super调用父类的构造方法,必须放在子类构造方法的第一行
    

super的注意事项

  • super访问成员变量和成员方法: 优先去父类中找,如果有就直接使用,如果没有就去爷爷类中找,如果有,就用,依次类推...

    class Ye{
        int num = 10;
        public void method(){
            System.out.println("Ye method");
        }
    }
    class Fu extends Ye{
        int num = 100;
        public void method(){
            System.out.println("Fu method");
        }
    }
    class Zi extends Fu{
        int num = 1000;
        public void show(){
            System.out.println(super.num);
            super.method();
        }
    }
    
    public class Test {
        public static void main(String[] args) {
    
            Zi zi = new Zi();
            zi.show();
        }
    }
    
  • 子类的构造方法默认会调用父类的空参构造方法,如果父类中的没有空参构造方法,只定义了有参构造方法,会编译报错

    class Fu1{
        public Fu1(){
            System.out.println("Fu1 空参构造");
        }
    
        public Fu1(int num){
            System.out.println("Fu1 有参构造");
        }
    }
    
    class Zi1 extends Fu1{
        public Zi1(){
            // super();
        }
    
        public Zi1(int num){
            // super();
        }
    }
    
    // 问题: super调用父类的构造方法有什么用?
    class Person{
        private String name;
        private int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void show(){
            System.out.println(name+","+age);
        }
    }
    
    class Student extends Person{
        public Student(String name,int age){
           super(name,age);
        }
    }
    
    public class Test2 {
        public static void main(String[] args) {
            /*
                super的注意事项二
                  1.子类的构造方法默认会调用父类的空参构造方法
                  2.如果父类中的没有空参构造方法,只定义了有参构造方法,会编译报错
                问题: super调用父类的构造方法有什么用?
                结果: 为了在创建子类对象的时候,初始化从父类继承过来的属性
             */
            // 通过调用子类的空参构造方法,创建子类对象
            // Zi1 zi = new Zi1();
    
            // 通过调用子类的有参构造方法,创建子类对象
            //Zi1 zi = new Zi1(100);
    
            // 创建Student类的对象
            Student stu = new Student("张三", 18);
            stu.show();
    
        }
    }
    
    
    
  • 子类构造方法中使用super调用父类的构造方法,是为了在创建子类对象的时候,初始化从父类继承过来的属性

继承体系对象的内存图

  • 继承体系内存图原理---父类空间优先于子类对象产生

    在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造方法调用时,一定先调用父类的构造方法。

  • 书写继承案例

    class Fu{
        int num = 10;
        int numFu = 100;
        public void method(){
            System.out.println("Fu method...");
        }
    }
    class Zi extends Fu{
        int num = 20;
        int numZi = 200;
        public void method(){
            System.out.println("Zi method...");
        }
        public void show(){
            int num = 30;
            System.out.println("局部变量num:"+num);// 30
            System.out.println("本类成员变量num:"+this.num);// 20
            System.out.println("父类成员变量num:"+super.num);// 10
            // 访问本类的method方法
            this.method();// Zi method...
            // 访问父类的method方法
            super.method();// Fu method...
        }
    }
    public class Test {
        public static void main(String[] args) {
            Zi zi = new Zi();
            zi.show();
        }
    }
    
    
    
  • 根据案例绘制内存图

    image-20200906123749033

继承的特点

  1. Java只支持单继承,不支持多继承。但是可以多层继承,java中所有类都是直接或者间接继承Object,所有类都是Object类的子类
  // 一个类只能有一个父类,不可以有多个父类。
class A {
    
}
class B {
    
}
class C1 extends A {// ok
    
} 
class C2 extends A, B {// error
    
} 
  1. 一个类只能有一个父类,但可以有多个子类。
  // A可以有多个子类
class A {
    
}
class C1 extends A {
    
}
class C2 extends  A {
    
}
  1. 可以多层继承。
class A /*extends Object*/{// 爷爷   默认继承Object类
    
}
class B extends A {// 父亲
    
}
class C extends B {// 儿子
    
}

补充: 顶层父类是Object类。所有的类默认继承Object,作为父类。

class A {} 默认继承Object类 直接继承Object类

class B extends A{} B的父类就是A,但是A的父类是Object类 间接继承Object类

java中所有类都是直接或者间接继承Object,所有类都是Object类的子类

抽象类

抽象类的概述和定义

抽象类的概述

  • 概述: 使用abstract关键字修饰的类就是抽象类
  • 特点: 这种类不能被创建对象,它就是用来做父类的,被子类继承的

抽象类的定义

  • 格式:

    修饰符 abstract class 类名{
        
    }
    
  • 例如:

    public abstract class Person{
    
    }
    

抽象类中的成员

  • 成员变量
  • 成员方法
  • 构造方法
  • 抽象方法
public abstract class Animal {
    // 成员变量
    private String name;
    private int age;
    // 构造方法
    public Animal(){

    }
    public Animal(String name,int age){
        this.name = name;
        this.age = age;
    }
    // 成员方法

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void show(){
        System.out.println(name+","+age);
    }
    // 抽象方法 ---??

}

public class Test {
    public static void main(String[] args) {
        /*
            抽象类:
                概述:使用abstract关键字修饰的类就是抽象类
                特点:抽象类不能创建对象,主要用来给子类继承的
                格式:
                    public abstract class 类名{
                        成员变量
                        构造方法
                        成员方法
                        抽象方法
                    }
               抽象类成员:
                    成员变量
                    构造方法
                    成员方法
                    抽象方法
              普通类和抽象类的区别:
                 1.普通类可以创建对象,抽象类不可以创建对象
                 2.普通类没有抽象方法,抽象类有抽象方法
         */
        //Animal anl1 = new Animal();// 编译报错,抽象类不能创建对象
        //Animal anl2 = new Animal("旺财",2);// 编译报错,抽象类不能创建对象
    }
}

抽象方法的概述和定义

抽象方法的概述

  • 没有方法体,使用abstract修饰的方法就是抽象方法

抽象方法的定义

修饰符 abstract 返回值类型 方法名(形参列表);
例如:
	public abstract void work();

抽象方法的作用: 强制要求子类重写的

public abstract class Animal {
    // 成员变量
    private String name;
    private int age;
    // 构造方法
    public Animal(){

    }
    public Animal(String name, int age){
        this.name = name;
        this.age = age;
    }
    // 成员方法

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    // 所有子类显示信息的方法实现都是一样的
    public void show(){
        System.out.println(name+","+age);
    }

    // 抽象方法 ---
    // 因为所有子类吃东西的方法实现不一样
    public abstract void eat();

}


public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }
}

package com.itheima.demo14_抽象方法的概述和定义;

/**
 * @Author:pengzhilin
 * @Date: 2020/9/6 14:55
 */
public class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("猫吃鱼...");
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            抽象方法:
                概述: 使用abstract修饰,并且没有方法体的方法
                格式: 修饰符 abstract 返回值类型 方法名(形参列表);
                抽象方法的使用场景:如果父类中某个方法,所有子类都有不同的实现,那么就可以把该方法定义为抽象方法
                抽象方法的作用: 强制要求子类重写

         */
        Dog d = new Dog();
        d.eat();

        Cat c = new Cat();
        c.eat();
    }
}

抽象类的注意事项

  • 抽象类不能被创建对象,就是用来做“父类”,被子类继承的。
  • 抽象类不能被创建对象,但可以有“构造方法”——为成员变量初始化。
  • 抽象类中可以没有抽象方法,但抽象方法必须定义在抽象类中
  • 子类继承抽象类后,必须重写抽象类中所有的抽象方法,否则子类必须也是一个抽象类
abstract class Animal{
    private String name;
    private int age;

    public Animal() {
    }

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show(){
        System.out.println(name+","+age);
    }

    // 抽象类没有抽象方法
}
class Dog extends Animal{
    public Dog() {
        super();
    }

    public Dog(String name, int age) {
        super(name, age);
    }
}

abstract class Person{
    // 抽象方法
    public abstract void eat();
    public abstract void drink();

}

//普通子类继承抽象类后,必须重写抽象类中所有的抽象方法
class Student extends Person{

    @Override
    public void eat() {
        // ...
    }

    @Override
    public void drink() {
        // ...
    }
}

//抽象子类继承抽象类后,可以不用重写抽象类中的抽象方法
 abstract class Teacher extends Person{
    @Override
    public void eat() {
        // ... 可以重写...
    }
}


public class Test {
    public static void main(String[] args) {
        /*
            抽象类的注意事项:
            - 抽象类不能被创建对象,就是用来做“父类”,被子类继承的。
            - 抽象类不能被创建对象,但可以有“构造方法”——为成员变量初始化。
            - 抽象类中可以没有抽象方法,但抽象方法必须定义在抽象类中(抽象类中不一定有抽象方法,但抽象方法一定在抽象类中)
            - 子类继承抽象类后,必须重写抽象类中所有的抽象方法,否则子类必须也是一个抽象类

         */
        // 抽象类不能被创建对象,就是用来做“父类”,被子类继承的。
        //Animal anl = new Animal();

        // 抽象类不能被创建对象,但可以有“构造方法”——为成员变量初始化。
        Dog d = new Dog("旺财", 2);
        d.show();// 旺财,2
    }
}

模板设计模式

设计模式概述

  • 设计模式就是解决一些问题时的固定思路,也就是代码设计思路经验的总结。

模板设计模式概述

  • 针对某些情况,在父类中指定一个模板,然后根据具体情况,在子类中灵活的具体实现该模板
public abstract class Person{
    // 有方法体的方法: 通用模板
    public void sleep(){
        System.out.println("两眼一闭,就睡觉...");
    }
    
    // 没有方法体的方法(抽象方法):  填充模板(要子类重新实现的)
   public abstract void eat();
}
  • 抽象类体现的就是模板设计思想模板是将通用的东西在抽象类中具体的实现,而模板中不能决定的东西定义成抽象方法,让使用模板(继承抽象类的类)的类去重写抽象方法实现需求

模板模式的实现步骤

  • 定义抽象父类作为模板
  • 在父类中定义"模板方法"--- 实现方法(通用模板)+抽象方法(填充模板)
  • 子类继承父类,重写抽象方法(填充父类的模板)
  • 测试类:
    • 创建子类对象,通过子类调用父类的“实现的方法”+ “子类重写后的方法” e

案例演示

假如我现在需要定义新司机和老司机类,新司机和老司机都有开车功能,开车的步骤都一样,只是驾驶时的姿势有点不同,新司机:开门,点火,双手紧握方向盘,刹车,熄火老司机:开门,点火,右手握方向盘左手抽烟,刹车,熄火。那么这个时候我们就可以将固定流程写到父类中,不同的地方就定义成抽象方法,让不同的子类去重写

分析:

  • 司机类
    • 开车方法: 确定实现--通用模板
      • 开门
      • 点火
      • (姿势)
      • 刹车
      • 熄火
    • 姿势方法: 不确定实现--填充模板
  • 新司机类继承司机类,重写姿势方法
  • 老司机类继承司机类,重写姿势方法
// 父类
public abstract class Driver {
    // 开车方法 通用模板
    public void driveCar(){
        System.out.println("开门");
        System.out.println("点火");
        // 姿势??
        ziShi();
        System.out.println("刹车");
        System.out.println("熄火");
    }

    // 姿势方法  填充模板
    public abstract void ziShi();
}

现在定义两个使用模板的司机:

public class NewDriver extends Driver {
    @Override
    public void ziShi() {
        System.out.println("双手紧握方向盘");
    }
}

public class OldDriver extends Driver {
    @Override
    public void ziShi() {
        System.out.println("右手握方向盘左手抽烟");
    }
}

编写测试类

public class Test {

    public static void main(String[] args) {
        // 创建新司机对象
        NewDriver d1 = new NewDriver();
        d1.driveCar();

        // 创建老司机对象
        OldDriver d2 = new OldDriver();
        d2.driveCar();
    }

}

运行效果

可以看出,模板模式的优势是,模板已经定义了通用架构,使用者只需要关心自己需要实现的功能即可!非常的强大!

final关键字

final: 不可改变。可以用于修饰类、方法和变量。

  • 类:被修饰的类,不能被继承。
  • 方法:被修饰的方法,不能被重写。
  • 变量:被修饰的变量,就只能赋值一次,不能被重新赋值。

修饰类

格式如下:

修饰符 final class 类名 {
  
}
例如:
public final class FinalClassFu {
}
public class FinalClassZi /*extends FinalClassFu*/ {
    // FinalClassFu类被final修饰了,所以不能被继承
}

查询API发现像 public final class Stringpublic final class Mathpublic final class Scanner 等,很多我们学习过的类,都是被final修饰的,目的就是供我们使用,而不让我们所以改变其内容。

修饰方法

格式如下:

修饰符 final 返回值类型 方法名(参数列表){
    //方法体
}

重写被 final修饰的方法,编译时就会报错。

public class FinalMethodFu {
    public final void show(){

    }
}
public class FinalMethodZi extends FinalMethodFu {

    /*@Override
    public void show() {

    }*/
    // 无法重写父类中的show方法,因为父类中的show方法被final修饰了
}

修饰变量

局部变量——基本类型

基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改。代码如下:

public class FinalDemo1 {
    public static void main(String[] args) {
         // final修饰基本数据类型
        final int NUM = 10;
        // NUM = 20;// 编译报错,final修饰的变量只能赋值一次,不能重复赋值
    }
}

局部变量——引用类型

引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改,代码如下:

public class FinalDemo2 {
    public static void main(String[] args) {
        // 引用类型
        final Student stu = new Student("张三",18);
        //stu = new Student("李四",19);// 编译报错
        stu.setAge(19);
    }
}

成员变量

成员变量涉及到初始化的问题,初始化方式有两种,只能二选一:

  1. 显示初始化;

    public class FinalVariable {
        final int NUM1 = 10;
    }
    
  2. 构造方法初始化。

    public class FinalVariable {
        final int NUM2;
        public FinalVariable(int NUM2){
            this.NUM2 = NUM2;
        }
        public FinalVariable(){
            this.NUM2 = 10;
        }
    }
    

被final修饰的常量名称,一般都有书写规范,所有字母都大写

static关键字

之前咋们写main方法的时候,使用过了一个static关键字,接下来我们来学习一下static关键字

static关键字概述

static是一个静态修饰符关键字,表示静态的意思,可以修饰成员变量和成员方法以及代码块。

static关键字的使用

static修饰成员变量

static 修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。

定义格式

static 数据类型 变量名; 

静态成员变量的访问方式

对象名.静态成员变量名; 不推荐
类名.静态成员变量名;  推荐

案例

public class Person {
    // 非静态变量
    String name;// 姓名
    // 静态变量
    static String country;// 国籍

    // 构造方法

    public Person() {
    }

    public Person(String name, String country) {
        this.name = name;
        this.country = country;
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建Person对象
        Person p1 = new Person("张三", "中国");
        System.out.println(p1.name+","+p1.country);// 张三,中国

        System.out.println("=======================");

        // 创建Person对象
        Person p2 = new Person();
        // 没有使用static修饰country
        // System.out.println(p2.name+","+p2.country);// null,null
        // 使用static修饰country
        System.out.println(p2.name+","+p2.country);// null,中国

        System.out.println("=======================");

        System.out.println(Person.country);// 中国
    }
}

image-20200708153852521

static修饰成员方法

被static修饰的方法会变成静态方法,也称为类方法,该静态方法可以使用类名直接调用。

格式

修饰符 static 返回值类型 方法名 (参数列表){ 
	// 执行语句 
}

访问方式

对象名.方法名(实参);
类名.方法名(实参);   推荐
public class Person {
    // 非静态方法
    public void method1(){
        System.out.println("Person method1...");
    }

    // 静态方法
    public static void method2(){
        System.out.println("Person method2...");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            static修饰成员方法:
                格式:修饰符 static 返回值类型 方法名(形参列表){方法体}
                特点:被static修饰的成员方法叫做静态成员方法
                使用:
                    对象名.静态成员方法名(实参);
                    类名.静态成员方法名(实参); ----->推荐
         */
        Person p = new Person();
        p.method2();

        // 类名.静态成员方法名(实参);
        Person.method2();

    }
}

静态方法调用的注意事项:

  • 静态方法中不能出现this关键字
  • 静态方法中只能直接访问静态成员变量和静态成员方法
  • 静态方法中不能直接访问非静态成员变量和非静态成员方法
  • 非静态方法中可以直接访问一切成员变量和成员方法
public class ChinesePeople {
    // 非静态成员变量
    String name;// 姓名
    // 静态成员变量
    static String country;// 国籍

    // 非静态方法
    public void method1(){
        System.out.println("非静态 method2方法");
    }

    public void method2(){
        // 非静态方法中可以直接访问一切成员变量和成员方法
        System.out.println(name);
        System.out.println(country);
        method1();
        method4();

        System.out.println("非静态 method2方法");
    }

    // 静态方法
    public static void method3(){
        //静态方法中不能直接访问非静态成员变量和非静态成员方法
        //System.out.println("非静态的成员变量:"+name);// 编译报错
        //method1();// 编译报错

        //静态方法中只能直接访问静态成员变量和静态成员方法
        System.out.println("静态成员变量:"+country);
        method4();

        // 静态方法中不能出现this关键字
        //System.out.println(this.name);// 编译报错
        //System.out.println(this.country);// 编译报错
        System.out.println("非静态 method3方法");
    }

    public static void method4(){
        System.out.println("非静态 method4方法");
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            概述: 被static修饰的方法就是静态方法,否则就是非静态方法
            static修饰成员方法: 在方法的返回值类型前面加上static
            访问静态方法:
                对象名.静态方法名(参数);  不推荐
                类名.静态方法名(参数);    推荐
            注意事项:
                1.静态方法中只能直接访问静态成员变量和静态成员方法
                2.静态方法中不能直接访问非静态成员变量和非静态成员方法
                3.非静态方法中可以直接访问一切成员变量和成员方法
                4.静态方法中不能出现this关键字
         */
        ChinesePeople.method3();

        //ChinesePeople p = new ChinesePeople();
        //p.method2();

        /*// 对象名.静态方法名(参数);  不推荐
        ChinesePeople p1 = new ChinesePeople();
        p1.method3();
        p1.method4();

        // 类名.静态方法名(参数);    推荐
        ChinesePeople.method3();
        ChinesePeople.method4();*/
    }

}

以后开发中static的应用

概述

以后的项目中,通常会需要一些“全局变量”或者“全局的工具方法”,这些全局变量和方法,可以单独定义在一个类中,并声明为static(静态)的,可以很方便的通过类名访问

例如:

public class Utils {
    // "全局变量"
    public static final int WIDTH = 800;
    public static final int HEIGHT = 800;


    // "全局方法"
    // 找int数组中的最大值
    public static int getArrayMax(int[] arr){
        int max = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] > max){
                max = arr[i];
            }
        }
        return max;
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            以后的项目中,通常会需要一些“全局变量”或者“全局的工具方法”,这些全局变量和方法,
            可以单独定义在一个类中,并声明为static(静态)的,可以很方便的通过类名访问

            工具类
         */
        System.out.println(Utils.width);
        System.out.println(Utils.height);

        int[] arr = {23,34,545,56};
        System.out.println(Utils.getArrayMax(arr));
    }
}

小结

static修饰成员方法:
	格式: 在返回值类型前面加static关键字
    使用: 类名.静态方法名(实参);
	注意事项:
		1.静态方法中不能出现this
        2.静态方法中只能直接访问静态成员变量和成员方法
        3.非静态方法中可以直接访问一切成员变量和成员方法
static修饰成员变量:
	格式: static 数据类型 变量名;
	使用; 类名.静态成员变量名
    特点; 被static修饰的变量会被该类的所有对象共享

接口

概述

引用数据类型除了类其实还有接口,接下来学习接口的概述

概述: 接口是Java语言中的一种引用类型,是方法的"集合",所以接口的内部主要就是定义方法,包含常量,抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法(jdk9)。

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

public class 类名{}-->.class

public interface 接口名{}->.class

引用数据类型:数组,类,接口。

接口的使用,它不能创建对象,但是可以被实现(implements ,类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。

小结

  • 接口是java语言中的一种引用数据类型
  • 接口中的成员:
    • 常量(jdk7及其以前)
    • 抽象方法(jdk7及其以前)
    • 默认方法和静态方法(jdk8额外增加)
    • 私有方法(jdk9额外增加)
  • 定义接口使用interface关键字---接口编译后产生class文件
  • 接口不能创建对象,需要使用实现类实现接口(类似于继承),实现接口的类叫做实现类(子类)

定义格式

格式

public interface 接口名称 {
    // 常量(jdk7及其以前)
    // 抽象方法(jdk7及其以前)
    // 默认方法(jdk8)
    // 静态方法(jdk8)
    // 私有方法(jdk9)
}

案例

public interface IA {
    // 常量(jdk7及其以前) 使用public static final关键字修饰,这三个关键字都可以省略
    public static final int NUM1 = 10;
    int NUM2 = 20;

    // 抽象方法(jdk7及其以前) 使用public abstract关键字修饰,这2个关键字都可以省略
    public abstract void method1();
    void method2();

    // 默认方法(jdk8) 使用public default关键字修饰,public可以省略,default不可以省略
    public default void method3(){
        System.out.println("默认方法 method3");
    }

    // 静态方法(jdk8) 使用public static关键字修饰,public可以省略,static不可以省略
    public static void method4(){
        System.out.println("静态方法 method4");
    }
    // 私有方法(jdk9) 使用private关键字修饰,private不可以省略
    private static void method5(){
        System.out.println("私有静态方法  method5");
    }

    private void method6(){
        System.out.println("私有非静态方法  method6");
    }
}

实现接口

实现概述

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

实现格式

  • 类可以实现一个接口,也可以同时实现多个接口。

    • 类实现接口后,必须重写接口中所有的抽象方法,否则该类必须是一个“抽象类”。

      public interface IA{
          public void show1();
      }
      public interface IB{
          public void show2();
      }
      public class Zi implements IA ,IB{
          public void show1(){
          }
          public void show2(){
          }
      }
      
  • 类可以在“继承一个类”的同时,实现一个、多个接口;

    public class Fu{}
    public interface IA{}
    public interface IB{}
    public class Zi extends Fu implements IA,IB{//一定要先继承,后实现
    }
    
    

接口中成员的访问特点

接口中成员访问特点概述

  接口中成员的访问特点:
                接口中的常量: 主要是供接口直接使用
                接口中的抽象方法: 供实现类重写的
                接口中的默认方法: 供实现类继承的(实现类中可以直接调用,实现类对象也可以直接调用)
                接口中的静态方法: 只供接口直接调用,实现类继承不了
                接口中的私有方法: 只能在接口中直接调用,实现类继承不了

案例演示

public interface IA {
    // 常量
    public static final int NUM = 10;

    // 抽象方法
    public abstract void method1();

    // 默认方法
    public default void method2(){
        //method4();
        //method5();
        System.out.println("IA 接口中的默认方法method2");
    }

    // 静态方法
    public static void method3(){
        //method5();
        System.out.println("IA 接口中的静态方法method3");
    }

    // 私有方法
    private void method4(){
        System.out.println("IA 接口中的私有方法method4");
    }

    private static void method5(){
        System.out.println("IA 接口中的私有方法method5");
    }
}


public class Imp implements IA {
    // 重写接口的抽象方法
    @Override
    public void method1() {
        System.out.println("实现类重写IA接口中的抽象方法");
    }

    // 重写接口的默认方法
    @Override
    public void method2() {
        System.out.println("实现类重写IA接口中的默认方法");
    }

}

public class Test {
    public static void main(String[] args) {
        /*
            接口中成员的访问特点:
                常量:主要是供接口名直接访问
                抽象方法:就是供实现类重写
                默认方法:就是供实现类重写或者实现类对象直接调用
                静态方法: 只供接口名直接调用
                私有方法: 只能在本接口中调用

         */
        // 访问接口常量
        System.out.println(IA.NUM);// 10  推荐
        //System.out.println(Imp.NUM);// 10 不推荐 常量被实现类继承了

        // 创建实现类对象调用方法
        Imp imp = new Imp();

        // 访问抽象方法
        imp.method1();

        // 访问默认方法
        imp.method2();

        // 接口名访问静态方法
        IA.method3();
        //Imp.method3();// 编译报错,没有继承
    }
}

小结

  • 接口中成员访问特点:
    • 常量:主要是供接口名直接访问
    • 抽象类:就是用来给实现类重写的
    • 默认方法:只供实现类重写或者实现类对象直接调用
    • 静态方法:只供接口名直接调用
    • 私有方法:只能在本接口中调用

多实现时的几种冲突情况

公有静态常量的冲突

  • 实现类不继承冲突的常量
interface A{
    public static final int NUM1 = 10;
}
interface B{
    public static final int NUM1 = 20;
    public static final int NUM2 = 30;
}
class Imp implements A,B{

}
public class Test {
    public static void main(String[] args) {
        /*
            公有静态常量的冲突: 如果多个接口中有相同的常量,那么实现类就无法继承
         */
        //System.out.println(Imp.NUM1);// 编译报错,无法访问
        System.out.println(Imp.NUM2);// 30
    }
}

公有抽象方法的冲突

  • 实现类只需要重写一个
interface A{
    public abstract void method();
}
interface B{
    public abstract void method();
}
class Imp implements A,B{
    @Override
    public void method() {
        System.out.println("实现类重写");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            公有抽象方法的冲突:实现类只需要重写一个 
         */
    }
}

公有默认方法的冲突

  • 实现类必须重写一次最终版本
interface A{
    public default void method(){
        System.out.println("A 接口的默认方法method");
    }
}
interface B{
    public default void method(){
        System.out.println("B 接口的默认方法method");
    }
}
class Imp implements A,B{
    @Override
    public void method() {
        System.out.println("实现类重写的默认方法");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            公有默认方法的冲突:实现类必须重写一次最终版本 
         */
        Imp imp = new Imp();
        imp.method();
    }
}

公有静态方法的冲突

  • 静态方法是直接属于接口的,不能被继承,所以不存在冲突

    interface A{
        public static void method(){
            System.out.println("A接口的静态方法method");
        }
    }
    interface B{
        public static void method(){
            System.out.println("B接口的静态方法method");
        }
    }
    class Imp implements A,B{
    
    }
    public class Test {
        public static void main(String[] args) {
            /*
                公有静态方法的冲突:静态方法是直接属于接口的,不能被继承,所以不存在冲突
             */
        }
    }
    
    

私有方法的冲突

  • 私有方法只能在本接口中直接使用,不存在冲突

小结

 多实现时的几种冲突情况:
                - 公有静态常量的冲突:实现类不继承冲突的常量
                - 公有抽象方法的冲突:实现类只需要重写一个
                - 公有默认方法的冲突:实现类必须重写一次最终版本
                - 公有静态方法的冲突:静态方法是直接属于接口的,不能被继承,所以不存在冲突
                - 私有方法的冲突:私有方法只能在本接口中直接使用,不存在冲突

接口和接口的关系

接口与接口之间的关系

  • 接口可以“继承”自另一个“接口”,而且可以“多继承”。

    interface IA{}
    interface IB{}
    interface IC extends IA,IB{//是“继承”,而且可以“多继承”
    }
    

接口多继承接口的冲突情况

公有静态常量的冲突
  • 子接口无法继承父接口中冲突的常量
interface A{
    public static final int NUM1 = 10;
}
interface B{
    public static final int NUM1 = 20;
    public static final int NUM2 = 30;
}
interface C extends A,B{

}
public class Test {
    public static void main(String[] args) {
        /*
            公有静态常量的冲突: 子接口无法继承父接口中冲突的常量
         */
        //System.out.println(C.NUM1);// 编译报错,说明无法继承
        System.out.println(C.NUM2);// 30
    }
}

公有抽象方法冲突
  • 子接口只会继承一个有冲突的抽象方法
interface A{
    public abstract void method();
}
interface B{
    public abstract void method();
}
interface C extends A,B{

}
class Imp implements C{
    @Override
    public void method() {
        System.out.println("实现接口的抽象方法");
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            公有抽象方法的冲突:子接口只会继承一个有冲突的抽象方法
         */
        Imp imp = new Imp();
        imp.method();
    }
}

公有默认方法的冲突
interface A{
    public default void method(){
        System.out.println("A 接口中的默认方法method");
    }
}
interface B{
    public default void method(){
        System.out.println("B 接口中的默认方法method");
    }
}

interface C extends A,B{

    @Override
    public default void method() {
        System.out.println("重写父接口中的method方法");
    }
}

class Imp implements C{

}

public class Test {
    public static void main(String[] args) {
        /*
            公有默认方法的冲突:子接口中必须重写一次有冲突的默认方法
            面试题:
                实现类重写接口中的默认方法,不需要加default
                子接口重写父接口中的面容方法,必须加default
         */
        Imp imp = new Imp();
        imp.method();// 重写父接口中的method方法
    }
}

公有静态方法和私有方法
  • 不冲突,因为静态方法是直接属于接口的,只能使用本接口直接访问,而私有方法只能在接口中访问,也没有冲突
interface A{
    public static void method(){
        System.out.println("A 接口的静态方法method");
    }
}
interface B{
    public static void method(){
        System.out.println("B 接口的静态方法method");
    }
}
interface C extends A,B{
    
}
public class Test {
    public static void main(String[] args) {
        /*
            公有静态方法的冲突: 不存在冲突,因为静态方法是直接属于接口的,只供本接口直接调用
         */
        //C.method();// 编译报错,因为没有继承
    }
}

小结

  - 接口与接口之间的关系: 继承关系
                    单继承: A接口继承B接口
                    多继承: A接口同时继承B接口,C接口,...
                    多层继承: A接口继承B接口,B接口,继承C接口
                    格式:
                        public interface 接口名 extends 接口名1,接口名2,...{
                            
                        }

- 接口多继承时的冲突情况
    - 公有静态常量的冲突:子接口无法继承父接口中冲突的常量
    - 公有抽象方法的冲突:子接口只会继承一个有冲突的抽象方法
    - 公有默认方法的冲突:子接口中必须重写一次有冲突的默认方法(注意要加default)
    - 公有静态方法和私有方法的冲突:
		不冲突,因为静态方法是直接属于接口的,只能使用本接口直接访问,而私有方法只能在接口中访问,也没有冲突

面试题:
     实现类重写接口中的默认方法,不需要加default
      子接口重写父接口中的默认方法,必须加default

实现类继承父类又实现接口时的冲突

父类和接口的公有静态常量的冲突

  • 子类无法继承有冲突的常量
class Fu{
    public static final int NUM1 = 10;
    public static final int NUM2 = 100;
}
interface A{
    public static final int NUM1 = 20;

}
class Zi extends Fu implements A{

}
public class Test {
    public static void main(String[] args) {
        /*
            公有静态常量的冲突:子类无法继承有冲突的常量
         */
        //System.out.println(Zi.NUM1);// 编译报错
        System.out.println(Zi.NUM2);

    }
}

父类和接口的抽象方法冲突

abstract class Fu{
    public abstract void method();
}
interface A{
    public abstract void method();
}
class Zi extends Fu implements A{
    @Override
    public void method() {
        System.out.println("Zi 重写有冲突的抽象方法");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            公有抽象方法的冲突:子类必须重写一次有冲突的抽象方法
         */
        Zi zi = new Zi();
        zi.method();
    }
}

父类和接口的公有默认方法的冲突

  • 优先访问父类的
class Fu{
    public void method(){
        System.out.println("Fu 类中的默认方法method");
    }
}
interface A{
    public default void method(){
        System.out.println("A 接口中的默认方法method");
    }
}
class Zi extends Fu implements A{

}
public class Test {
    public static void main(String[] args) {
        /*
            公有默认方法的冲突:优先访问父类的
         */
        Zi zi = new Zi();
        zi.method();// Fu 类中的默认方法method
    }
}

父类和接口的公有静态方法

  • 只会访问父类的静态方法
class Fu{
    public static void method(){
        System.out.println("Fu 类中的静态方法method");
    }
}
interface A{
    public static void method(){
        System.out.println("A 接口中的静态方法method");
    }
}
class Zi extends Fu implements A{
    
}
public class Test {
    public static void main(String[] args) {
        /*
            公有静态方法的冲突:只会访问父类的静态方法
         */
        Zi.method();
    }
}

父类和接口的私有方法

  • 不存在冲突

小结

  实现类继承父类又实现接口时的冲突:
                - 公有静态常量的冲突:子类无法继承有冲突的常量
                - 公有抽象方法的冲突:子类必须重写一次有冲突的抽象方法
                - 公有默认方法的冲突:优先访问父类的
                - 公有静态方法的冲突:只会访问父类的静态方法
                - 私有方法的冲突: 不存在冲突

抽象类和接口的练习

需求

通过实例进行分析和代码演示抽象类和接口的用法。

1、举例:

​ 犬: ---抽象父类

​ 行为:吼叫;吃饭;

​ 缉毒犬:继承犬类,实现缉毒接口

​ 行为:吼叫;吃饭;缉毒;

​ 缉毒接口:

​ 缉毒

  • 如果一个父类中的某个方法,所有子类都有不同的实现,那么该方法就应该定义成抽象方法,所以该父类就是抽象类 (父类一般都是抽象类)
  • 如果某个功能是一个类额外增加的,那么就可以把这个额外的功能定义到接口中,再这个类去实现

分析

​ 由于犬分为很多种类,他们吼叫和吃饭的方式不一样,在描述的时候不能具体化,也就是吼叫和吃饭的行为不能明确。当描述行为时,行为的具体动作不能明确,这时,可以将这个行为写为抽象行为,那么这个类也就是抽象类。

​ 可是有的犬还有其他额外功能,而这个功能并不在这个事物的体系中 , 例如 : 缉毒犬。缉毒的这个功能有好多种动物都有 , 例如 : 缉毒猪 , 缉毒鼠。我们可以将这个额外功能定义接口中 ,让缉毒犬继承犬且实现缉毒接口 , 这样缉毒犬既具备犬科自身特点也有缉毒功能。

  • 额外的功能---> 在接口中定义,让实现类实现
  • 共性的功能---> 在父类中定义,让子类继承

实现

// 抽象父类
public abstract class Dog {
    public abstract void houJiao();
    public abstract void eat();
}


public interface JiDu {
    public abstract void jiDu();
}

public class JiDuDog extends Dog implements JiDu{
    @Override
    public void houJiao() {
        System.out.println("缉毒犬找到了毒品,开始吼叫,汪汪汪....");
    }

    @Override
    public void eat() {
        System.out.println("缉毒之前,开始吃骨头...");
    }

    @Override
    public void jiDu() {
        System.out.println("吃完东西后,开始使用鼻子查找毒品....");
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建缉毒狗对象
        JiDuDog jd = new JiDuDog();
        jd.eat();
        jd.jiDu();
        jd.houJiao();
    }
}

小结

  • 额外的功能---> 在接口中定义,让实现类实现
    • 如果可以确定的通用功能,使用默认方法
    • 如果不能确定的功能,使用抽象方法
  • 共性的功能---> 在父类中定义,让子类继承
    • 如果可以确定的通用功能,使用默认方法
    • 如果不能确定的功能,使用抽象方法

多态

概述

多态是继封装、继承之后,面向对象的第三大特性。

生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。多态,描述的就是这样的状态。

定义

  • 多态: 是指同一行为,对于不同的对象具有多个不同表现形式。
  • 程序中多态: 是指同一方法,对于不同的对象具有不同的实现.

前提条件【重点】

  1. 继承或者实现【二选一】
  2. 父类引用指向子类对象\接口引用指向实现类对象【格式体现】
  3. 方法的重写【意义体现:不重写,无意义】

实现多态

多态的体现:父类的引用指向它的子类的对象

父类类型 变量名 = new 子类对象;
变量名.方法名();

父类类型:指子类对象继承的父类类型,或者实现的父接口类型。

class Animal{
    public void eat(){
        System.out.println("吃东西");
    }
}

class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }
}

class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        // 父类引用指向子类对象
        Animal anl = new Dog();// 多态
        anl.eat();// 狗吃骨头...

        Animal anl1 = new Cat();
        anl1.eat();// 猫吃鱼...
    }
}

多态时访问成员的特点

1_多态时成员访问特点执行流程图

  • 多态时成员变量的访问特点
    • 编译看左边,运行看左边
      • 简而言之:多态的情况下,访问的是父类的成员变量
  • 多态时成员方法的访问特点
    • 非静态方法:编译看左边,运行看右边
      • 简而言之:编译的时候去父类中查找方法,运行的时候去子类中查找方法来执行
    • 静态方法:编译看左边,运行看左边
      • 简而言之:编译的时候去父类中查找方法,运行的时候去父类中查找方法来执行
  • 注意:多态的情况下是无法访问子类独有的方法

除了非静态方法是编译看父类,运行看子类,其余都是看父类

演示代码:

class Animal{
    int num = 10;
    public void method1(){
        System.out.println("Animal 非静态method1方法");
    }
    public static void method2(){
        System.out.println("Animal 静态method2方法");
    }
}
class Dog extends Animal{
    int num = 20;

    public void method1(){
        System.out.println("Dog 非静态method1方法");
    }

    public static void method2(){
        System.out.println("Dog 静态method2方法");
    }
}

public class Test {
    public static void main(String[] args) { 
        Animal anl = new Dog();
        System.out.println(anl.num);// 10

        anl.method1();// Dog 非静态method1方法
        anl.method2();// Animal 静态method2方法
    }
}

多态的表现形式

普通父类多态

public class Fu{}
public class Zi extends Fu{}
public class Demo{
    public static void main(String[] args){
        Fu f = new Zi();//左边是一个“父类”
    }
}

抽象父类多态

public abstract class Fu{}
public class Zi extends Fu{}
public class Demo{
    public static void main(String[] args){
        Fu f = new Zi();//左边是一个“父类”
    }
}

父接口多态

public interface A{}
public class AImp implements A{}
public class Demo{
    public static void main(String[] args){
        A a = new AImp();
    }
}

多态的应用场景

变量多态 -----> 意义不大

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}
public class Test {
    public static void main(String[] args) {
        // 变量多态: 父类类型的变量指向子类类型的对象
        // 如果变量的类型为父类类型,该变量就可以接收该父类类型的对象或者其所有子类对象
        Animal anl = new Dog();
        anl.eat();

        anl = new Cat();
        anl.eat();
    }
}

形参多态----> 常用

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}
public class Test {
    public static void main(String[] args) {
        // 形参多态:参数类型为父类类型,该参数就可以接收该父类类型的对象或者其所有子类对象
        Dog d = new Dog();
        method(d);

        System.out.println("===============================");

        Cat c = new Cat();
        method(c);
    }

    // 需求: 定义一个方法,带有一个参数,该参数可以接收Animal类对象以及Animal类的所有子类对象
    // method(d); ====实参赋值给形参的时候==> Animal anl = new Dog();
    // method(c); ====实参赋值给形参的时候==> Animal anl = new Cat();
    public static void method(Animal anl){
        anl.eat();
    }

}

返回值多态---> 常用

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头");
    }
}
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}
public class Test {
    public static void main(String[] args) {
        // 返回值多态:如果返回值类型为父类类型,那么就可以返回该父类类型的对象或者其所有子类对象
        Animal anl = method();
        anl.eat();
    }

    public static Animal method(){
        //return new Animal();
        //return new Dog();
        return new Cat();
    }

    public static Animal method1(){
        if (1==1){
            // 条件1成立
            return new Animal();
        }else if (2==2){
            // 条件2成立
            return new Dog();
        }else{
            // 否则
            return new Cat();
        }
    }
}

多态的好处和弊端

好处

  • 提高了代码的扩展性

弊端

  • 多态的情况下,无法访问子类独有的方法或者成员变量,因为多态成员访问的特点是,编译看父类

示例代码

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }

    // 特有的功能
    public void lookHome(){
        System.out.println("狗在看家...");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            多态的好处和弊端:
                好处:提高代码的复用性
                弊端:无法访问子类独有的方法或者成员变量,因为多态成员访问的特点是,编译看父类
         */
        // 父类的引用指向子类的对象
        Animal anl = new Dog();
        anl.eat();
        //anl.lookHome();// 编译报错,因为多态成员访问的特点是,编译看父类,而父类中没有子类独有的功能
    }
}

引用类型转换

向上转型

子类类型向父类类型向上转换的过程,这个过程是默认的。

 Aniaml anl = new Cat();  

向下转型

父类类型向子类类型向下转换的过程,这个过程是强制的。

 Aniaml anl = new Cat();  
 Cat c = (Cat)anl;//向下转型

示例代码

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }

    // 特有的功能
    public void lookHome(){
        System.out.println("狗在看家...");
    }
}
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼...");
    }
}

class Person{}
public class Test {
    public static void main(String[] args) {
        /*
            引用类型转换:
                向上转型:子类类型向父类类型向上转换的过程,这个过程是默认\自动的。
                向下转型:父类类型向子类类型向下转换的过程,这个过程是强制\手动的。
                        格式: 子类类型 对象名 = (子类类型)父类类型的变量;
                        注意:
                            1.向下转型的时候:右边父类类型的变量一定要指向要转型的子类类型的对象
                            2.不管是向上转型还是向下转型,一定满足父子类关系或者实现关系
         */
        // 向上转型:
        Animal anl = new Dog();

        // 向下转型:
        Dog dog = (Dog)anl;

        System.out.println("===================================");
        // 注意:右边父类类型的变量一定要指向要转型的子类类型的对象
        //Animal anl1 = new Animal();
        //Dog d1 = (Dog)anl1;// 运行报错,类型转换异常ClassCastException


        //Animal anl2 = new Cat();
        //Dog d2 = (Dog)anl2;// 运行报错,类型转换异常ClassCastException

        //Animal anl3 = new Person();// 编译报错,因为Animal和Person不是父子关系
        //Animal anl3 = (Animal) new Person();// 编译报错,因为Animal和Person不是父子关系

    }
}

instanceof关键字

向下强转有风险,最好在转换前做一个验证 :

格式:

变量名 instanceof 数据类型 
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。

if( anl instanceof Cat){//判断anl是否能转换为Cat类型,如果可以返回:true,否则返回:false
    Cat c = (Cat)anl;//安全转换
}

示例代码

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }

    // 特有的功能
    public void lookHome(){
        System.out.println("狗在看家...");
    }
}
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼...");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            instanceof关键字:
                为什么要有instanceof关键字?
                    因为在引用类型转换的时候很容易出现类型转换异常,所以为了提高代码的严谨性,转型之前得先判断一下
                怎么使用instanceof关键字判断呢?
                    if(变量名 instanceof 数据类型){

                    }
                 执行:
                    判断前面变量指向的对象类型是否是后面的数据类型:
                        如果前面变量指向的对象类型是属于后面的数据类型,那么就返回true
                        如果前面变量指向的对象类型不是属于后面的数据类型,那么就返回false
         */
        // 向上转型
        Animal anl = new Cat();

        // 向下转型
        //Dog  d = (Dog)anl;// 运行的时候会出现类型转换异常
        // 先判断,再转型
        if (anl instanceof Dog){
            Dog  d = (Dog)anl;
        }

        System.out.println("正常结束");
    }
}

小结

 引用类型转换:
	向上转型:子类类型向父类类型向上转换的过程,这个过程是默认\自动的。
    向下转型:父类类型向子类类型向下转换的过程,这个过程是强制\手动的。
        格式: 子类类型 对象名 = (子类类型)父类类型的变量;
注意:
	1.向下转型的时候:右边父类类型的变量一定要指向要转型的子类类型的对象
    2.不管是向上转型还是向下转型,一定满足父子类关系或者实现关系

instanceof关键字:
	if(变量名 instanceof 数据类型){}
    如果变量属于该数据类型,返回true。
    如果变量不属于该数据类型,返回false。

解决多态的弊端

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }

    // 特有的功能
    public void lookHome(){
        System.out.println("狗在看家...");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            解决多态的弊端:
                弊端:无法访问子类独有的方法或者成员变量,因为多态成员访问的特点是,编译看父类
         */
        // 父类的引用指向子类的对象
        Animal anl = new Dog();// 向上转型
        anl.eat();// 狗吃骨头...

        //anl.lookHome();// 编译报错,因为多态成员访问的特点是,编译看父类,而父类中没有子类独有的功能

        // 先判断,后转型
        if (anl instanceof Dog){
            Dog d = (Dog)anl;// 向下转型
            d.lookHome();// 狗在看家...
        }

        System.out.println("正常结束");
    }
}

多态的应用场景综合案例

class Animal{
    public void eat(){
        System.out.println("吃东西...");
    }
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }

    // 特有的功能
    public void lookHome(){
        System.out.println("狗在看家...");
    }
}
class Cat extends Animal{
    @Override
    public void eat() {
        System.out.println("猫吃鱼...");
    }
    // 特有的功能
    public void catchMouse(){
        System.out.println("猫抓老鼠...");
    }
}
public class Test {
    public static void main(String[] args) {
        Dog d = new Dog();
        method(d);

        System.out.println("==========================");

        Cat c = new Cat();
        method(c);
    }

    // 形参多态: 如果父类类型作为方法的形参类型,那么就可以接收该父类类型的对象或者其所有子类的对象
    public static void method(Animal anl){
        anl.eat();
        //anl.lookHome();// 编译报错
        // anl.catchMouse();// 编译报错
        if (anl instanceof Dog){
            Dog d = (Dog)anl;// 向下转型 Dog类型
            d.lookHome();
        }

        if (anl instanceof Cat){
            Cat c = (Cat)anl;// 向下转型 Cat类型
            c.catchMouse();
        }

    }
}

内部类

什么是内部类

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类

成员内部类

  • 成员内部类 :定义在类中方法外的类。

定义格式:

class 外部类 {
    class 内部类{

    }
}

在描述事物时,若一个事物内部还包含其他事物,就可以使用内部类这种结构。比如,汽车类Car 中包含发动机类Engine ,这时,Engine 就可以使用内部类来描述,定义在成员位置。

代码举例:

class Car { //外部类
    class Engine { //内部类

    }
}

访问特点

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。

创建内部类对象格式:

外部类名.内部类名 对象名 = new 外部类型().new 内部类型();

访问演示,代码如下:

// 外部类
public class Body {

    public void methodW1(){
        // 访问内部类的成员
        //Body.Heart bh = new Body().new Heart();
        Heart bh = new Heart();
        System.out.println(bh.numN);// 10
        bh.methodN1();// 内部类的成员方法 methodN1
    }

    // 成员变量
    private int numW = 100;

    // 成员方法
    private void methodW2(){
        System.out.println("外部类的成员方法 methodW2");
    }


    // 内部类
    public class Heart{
        // 成员变量
        int numN = 10;

        // 成员方法
        public void methodN1(){
            System.out.println("内部类的成员方法 methodN1");
        }

        public void methodN2(){
            // 访问外部类的成员
            System.out.println(numW);
            methodW2();
        }
    }


}


public class Test {
    public static void main(String[] args) {
        /*
            - 什么是内部类:将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,外面的那个B类则称为外部类。
            - 成员内部类的格式:
                 public class 外部类{
                     public class 内部类{

                    }
                 }
            - 成员内部类的访问特点:
                在其他类中,访问内部类的成员,得先创建内部类对象:
                    外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
                在外部类中,访问内部类的成员,得先创建内部类对象:
                    外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
                    内部类名 对象名 = new 内部类名();

                在内部类中,可以直接访问外部类的一切成员(包含私有的):

         */
        // 创建内部类的对象
        Body.Heart bh = new Body().new Heart();
        System.out.println(bh.numN);// 10
        bh.methodN1();// 内部类的成员方法 methodN1

        System.out.println("=======================");
        // 创建外部类对象
        Body b = new Body();
        b.methodW1();

        System.out.println("=======================");
        bh.methodN2();// 100    外部类的成员方法 methodW2

    }
}

匿名内部类

是内部类的简化写法。它的本质是一个带具体实现的 父类或者父接口的 匿名的 子类对象

代码一:

abstract class Animal{
    public abstract void eat();
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            匿名内部类:
                概述:本质其实就是一个类的匿名子类的对象
                作用:就是用来简化代码的,没有其他的功能
                格式:
                    new 类名(){
                        实现抽象方法
                    };
         */
        // 需求:调用Animal类的eat方法
        // 1.创建一个子类继承Animal类
        // 2.在子类中重写eat抽象方法
        // 3.创建子类对象
        // 4.使用子类对象调用eat方法
        Dog d = new Dog();// 创建Animal子类对象
        d.eat();// d---->是Animal类的子类的对象
        // 问题:以上4步一步都不能少,有点麻烦,是否可以简化代码?
        // 解决:匿名内部类可以简化代码,因为它可以不创建子类的情况下,直接得到一个类的子类对象

        System.out.println("==========================");
        // 创建Animal子类对象<=====>Animal类的匿名内部类
        // 父类的引用指向子类的对象
        Animal anl = new Animal() {
            @Override
            public void eat() {
                System.out.println("匿名内部类");
            }
        };// 是Animal类的子类的对象
        anl.eat();
    }
}

代码二:

interface A{
    public abstract void show();
}
class Imp implements A{
    public void show(){
        System.out.println("实现类实现show方法");
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            匿名内部类:
                概述:本质是一个接口的匿名实现类的对象
                格式:
                    new 接口名(){
                        实现抽象方法
                    };
         */
        // 需求:调用A接口的show方法
        // 1.创建实现类实现A接口
        // 2.在实现类中重写show方法
        // 3.创建实现类对象
        // 4.使用实现类对象调用show方法
        Imp imp = new Imp();// imp就是接口的实现类的对象
        imp.show();

        System.out.println("==============================");
        // 简化: 匿名内部类
        A a = new A() {
            @Override
            public void show() {
                System.out.println("匿名内部类");
            }
        };
        a.show();
    }
}

小结:

对于类:
	概述:本质其实就是一个类的匿名子类的对象
    格式:
        new 类名(){
            实现抽象方法
        };
对于接口:
	概述:本质是一个接口的匿名实现类的对象
    格式:
        new 接口名(){
            实现抽象方法
        };
匿名内部类作用:就是用来简化代码的,没有其他的功能
使用场景:
	如果方法的形参类型为抽象类或者接口类型,那么为了简化代码,可以直接传入该抽象类或者接口的匿名内部类

补充

// 匿名子类的匿名对象
new Imp().show();// 实现类的匿名对象调用show方法
new A() {
    @Override
    public void show() {
        System.out.println("匿名内部类");
    }
}.show();// 匿名实现类的匿名对象调用show方法

引用类型使用

​ 实际的开发中,引用类型的使用非常重要,也是非常普遍的。我们可以在理解基本类型的使用方式基础上,进一步去掌握引用类型的使用方式。基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的。在这我们使用两个例子 , 来学习一下。

类名作为方法参数和返回值

class Person{
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void show(){
        System.out.println(name+","+age);
    }
}
public class Test {
    public static void main(String[] args) {
        /*
            类名作为方法参数和返回值:
         */
        // 创建Person
        Person p = new Person("冰冰",18);
        method1(p);
        System.out.println("=========================================");
        // 调用method2;
        Person person = method2(p);
        person.show();// 冰冰,20
    }

    // 类作为方法的参数类型
    public static void method1(Person p){
        p.show();// 冰冰,18
    }

    // 类作为方法的参数类型和返回值类型
    public static Person method2(Person p){
        p.age = 20;// 把age改为20
        return p;
    }
}

抽象类作为方法参数和返回值

  • 抽象类作为形参:表示可以接收任何此抽象类的"子类对象"作为实参;
  • 抽象类作为返回值:表示"此方法可以返回此抽象类的任何子类对象";
abstract class Animal{
    public abstract void eat();
}
class Dog extends Animal{
    @Override
    public void eat() {
        System.out.println("狗吃骨头...");
    }
}
public class Test {
    public static void main(String[] args) {
        // 调用method1,就得传入Animal抽象类的子类对象
        method1(new Dog());

        System.out.println("========================");
        // 调用method1,就得传入Animal抽象类的子类对象
        method1(new Animal() {
            @Override
            public void eat() {
                System.out.println("匿名内部类的方式...");
            }
        });

        System.out.println("========================");
        // 调用method2方法,会返回一个Animal类的子类对象
        //Animal anl = method2();
        Dog d = (Dog)method2();
    }

    // 抽象类作为方法参数类型
    public static void method1(Animal anl){
        anl.eat();
    }

    // 抽象类作为方法返回值类型
    public static Animal method2(){
        return new Dog();
    }
}

接口作为方法参数和返回值

  • 接口作为方法的形参:【同抽象类】
  • 接口作为方法的返回值:【同抽象类】
interface A{
    void show();
}
class Imp implements A{
    public void show(){
        System.out.println("实现类的方式实现show方法");
    }
}
public class Test {
    public static void main(String[] args) {
        // 接口作为方法参数和返回值
        // 调用method1方法,就得传入A接口的实现类对象
        method1(new Imp());

        System.out.println("===================");

        // 调用method1方法,就得传入A接口的匿名内部类
        method1(new A() {
            @Override
            public void show() {
                System.out.println("匿名内部类的方式实现show方法");
            }
        });

        System.out.println("===================");

        // 调用method2方法,就会返回A接口的实现类对象
        //A a = method2();
        Imp imp = (Imp) method2();


    }

    // 接口作为方法参数
    public static void method1(A a){
        a.show();
    }

    // 接口作为方法返回值
    public static A method2(){
        return new Imp();
    }
}

类名作为成员变量

​ 我们每个人(Person)都有一个身份证(IDCard) , 为了表示这种关系 , 就需要在Person中定义一个IDCard的成员变量。定义Person类时,代码如下:

class Person {
    String name;//姓名
    int age;//年龄
}

​ 使用String 类型表示姓名 , int 类型表示年龄。其实,String本身就是引用类型,我们往往忽略了它是引用类型。如果我们继续丰富这个类的定义,给Person 增加身份证号 , 身份证签发机关等属性,我们将如何编写呢?这时候就需要编写一个IDCard类了

修改Person类:

class Person{
    String name;// 引用数据类型定义成员变量  String类
    int age;// 基本类型定义成员变量
    IdCard idCard;

    public Person(String name, int age, IdCard idCard) {
        this.name = name;
        this.age = age;
        this.idCard = idCard;
    }
    // ...
}
class IdCard{
    String idNum;// 身份证号码
    String address;// 地址

    public IdCard(String idNum, String address) {
        this.idNum = idNum;
        this.address = address;
    }
    // ....
}
public class Test {
    public static void main(String[] args) {
        // 创建IdCard对象
        IdCard idCard = new IdCard("440330200010101919","广东省深圳市宝安区公安局");
        // 创建Person对象
        Person p = new Person("张三",18,idCard);
        System.out.println(p.name+","+p.age+","+p.idCard.idNum+","+p.idCard.address);// java支持链式编程
    }
}

类作为成员变量时,对它进行赋值的操作,实际上,是赋给它该类的一个对象。同理 , 接口也是如此 , 例如我们笔记本案例中使用usb设备。在此我们只是通过小例子 , 让大家熟识下引用类型的用法 , 后续在咱们的就业班学习中 , 这种方式会使用的很多。

抽象类作为成员变量

  • 抽象类作为成员变量——为此成员变量赋值时,可以是任何它的子类对象
abstract class Pet{
    String name;

    public Pet(String name) {
        this.name = name;
    }
}
class Dog extends Pet{

    public Dog(String name) {
        super(name);
    }
}
class Person{
    String name;
    int age;
    Pet pet;

    public Person(String name, int age, Pet pet) {
        this.name = name;
        this.age = age;
        this.pet = pet;
    }
}
public class Test {
    public static void main(String[] args) {
        // 抽象类作为成员变量:传入抽象类的子类对象
        Pet pet = new Dog("旺财");
        Person p = new Person("张三",18,pet);
        System.out.println(p.name);
        System.out.println(p.age);
        System.out.println(p.pet.name);

    }
}

接口作为成员变量

  • 接口类型作为成员变量——【同抽象类】
abstract interface Pet{

}
class Dog implements Pet{


}
class Person{
    String name;
    int age;
    Pet pet;

    public Person(String name, int age, Pet pet) {
        this.name = name;
        this.age = age;
        this.pet = pet;
    }
}
public class Test {
    public static void main(String[] args) {
        // 接口作为成员变量:传入接口的实现类对象
        Pet pet = new Dog();
        Person p = new Person("张三",18,pet);
        System.out.println(p.name);
        System.out.println(p.age);
        System.out.println(p.pet);

    }
}

英雄: name,皮肤,法术(接口)

小结

- 类名作为方法参数和返回值:可以直接传入该类的对象;返回该类的对象
- 抽象类作为方法参数和返回值:只能传入该类的子类对象;返回该类的子类对象
- 接口作为方法参数和返回值:只能传入该接口的实现类对象;返回该接口的实现类对象
    传递的都是地址值,返回的也是地址值
          
- 类作为成员变量    : 赋该类的对象
- 抽象类作为成员变量 ; 赋该类的子类对象
- 接口作为成员变量   : 赋该接口的实现类对象

权限修饰符

在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,

  • public:公共的
  • protected:受保护的
  • (空的):默认的
  • private:私有的

不同权限的访问能力

public protected (空的) private
同一类中
同一包中(子类与无关类)
不同包的子类
不同包中的无关类
包:com.nbchen.demo9_权限修饰符
public class AAA {
    public void method1(){}
    protected void method2(){}
     void method3(){}
    private void method4(){}

    // 同一个类中
    public void method(){
        method1();
        method2();
        method3();
        method4();
    }
}
public class Test {
    public static void main(String[] args) {
        AAA a = new AAA();
        a.method1();
        a.method2();
        a.method3();
        // a.method4(); 私有方法 编译报错
    }
}

包:com.nbchen.demo10_权限修饰符
public class Zi extends AAA {
    public void show(){
        method1();
        method2();
        //method3();编译报错
        //method4();编译报错
    }
}
public class Test {
    public static void main(String[] args) {
        AAA a = new AAA();
        a.method1();
        //a.method2();// 编译报错
        //a.method3();// 编译报错
        //a.method4();// 编译报错
    }
}

可见,public具有最大权限。private则是最小权限。

编写代码时,如果没有特殊的考虑,建议这样使用权限:

  • 成员变量使用private ,隐藏细节。

  • 构造方法使用 public ,方便创建对象。

  • 成员方法使用public ,方便调用方法。

代码块

构造代码块

格式: {}
位置: 类中,方法外
执行: 每次在调用构造方法的时候,就会执行
使用场景: 统计创建了多少个该类对象

例如:
public class Person{
    {
        构造代码块执行了
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            构造代码块:
                格式: {}
                位置: 类中,方法外
                执行: 每次执行构造方法之前都会执行一次
                使用场景: 例如统计对象的个数 也就是每次执行构造方法之前要执行的代码就可以放在构造代码块中
         */
        Person p1 = new Person();
        Person p2 = new Person();
    }
}

静态代码块

格式:static{}
位置: 类中,方法外
执行: 当类被加载的时候执行,并只执行一次
使用场景: 例如加载驱动,这种只需要执行一次的代码就可以放在静态代码块中         

public class Person {
    static {
        System.out.println("Person 静态代码块");
    }

    {
        System.out.println("Person 构造代码块");
    }

    public Person(){
        System.out.println("Person 构造方法");
    }
}

public class Test {
    /*
         静态代码块:
            格式: static{}
            位置: 类中,方法外
            执行: 随着类的加载而执行,并且只执行一次
            使用场景: 例如读取配置文件中的数据,加载驱动,也就是说程序中只需要执行一次的代码就可以放在静态代码块中

            执行优先级:  静态代码块 >  构造代码块  >  构造方法
     */
    public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Person();

    }
}

局部代码块

格式:{}
位置: 方法中
执行: 调用方法,执行到局部代码块的时候就执行
使用场景: 节省内存空间,没有多大的意义
例如:
public class Test {
    public static void main(String[] args) {
        /*
            局部代码块:
                格式: {}
                位置: 方法中
                执行: 调用方法,执行到了局部代码块的时候执行
                使用场景: 节省内存空间,没有太多意义
         */
        System.out.println("开始");
        {
            int num1 = 10;
            System.out.println("局部代码块");
        }// 把局部代码块中的变量占用的空间会释放

        System.out.println("结束");
    }
}

集合

集合的关注点

1.是否允许为空

2.是否允许重复数据

3.是否有序(有序的意思是读取数据的顺序和存放数据的顺序是否一致)

4.是否线程安全

Image

Collection集合

集合概述

在前面基础班我们已经学习过并使用过集合ArrayList<E> ,那么集合到底是什么呢?

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

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

  • 数组的长度是固定的。集合的长度是可变的。
  • 集合存储的都是引用数据类型。如果想存储基本类型数据需要存储对应的包装类类型。

单列集合常用类的继承体系

Collection:是单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是

  • java.util.List: List的特点是元素有序、元素可重复 ;
    • List接口的主要实现类有java.util.ArrayListjava.util.LinkedList
  • java.util.Set: Set的特点是元素不可重复
    • Set接口的主要实现类有java.util.HashSetjava.util.LinkedHashSetjava.util.TreeSet

为了便于初学者进行系统地学习,接下来通过一张图来描述集合常用类的继承体系

image-20220709082818693

注意:上面这张图只是我们常用的集合有这些,不是说就只有这些集合。

单列集合常用类的继承体系:
	Collection集合:接口,是所有单列集合的顶层父接口,该集合中的方法可以被所有单列集合共享
    	List集合: 接口,元素可重复,元素有索引,元素存取有序
        	ArrayList集合: 实现类,查询快,增删慢
            LinkedList集合: 实现类,查询慢,增删快

        Set集合: 接口, 元素不可重复(唯一),元素无索引
            HashSet集合: 实现类,元素存取无序
            LinkedHashSet集合:实现类,元素存取有序
            TreeSet集合:实现类,可以对集合中的元素进行排序

Collection 常用功能

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

  • public boolean add(E e): 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中

tips: 有关Collection中的方法可不止上面这些,其他方法可以自行查看API学习。

public class Test {
    public static void main(String[] args) {
        // 创建Collection集合对象,限制集合中元素的类型为String
        Collection<String> col = new ArrayList<>();

        // 往col集合中添加元素
        col.add("范冰冰");
        col.add("李冰冰");
        col.add("林心如");
        col.add("赵薇");

        System.out.println("col集合:"+col);// col集合:[范冰冰, 李冰冰, 林心如, 赵薇]

        // 清空集合中所有的元素
        //col.clear();
        //System.out.println("col集合:"+col);// col集合:[]

        // 删除李冰冰这个元素
        col.remove("李冰冰");
        System.out.println("col集合:"+col);// col集合:[范冰冰, 林心如, 赵薇]

        // 判断col集合中是否包含李冰冰这个元素
        boolean res1 = col.contains("李冰冰");
        System.out.println("res1:"+res1);// false
        // 判断col集合中是否包含林心如这个元素
        boolean res2 = col.contains("林心如");
        System.out.println("res2:"+res2);// true

        //判断当前集合是否为空。(判断集合中是否有元素)
        boolean res3 = col.isEmpty();
        System.out.println("res3:"+res3);// false
        /*col.clear();// 清空元素
        boolean res4 = col.isEmpty();
        System.out.println("res4:"+res4);// true*/

        // 获取集合中元素的个数
        System.out.println("集合中元素的个数:"+col.size());// 3

        // 把集合中的元素,存储到数组中
        Object[] arr = col.toArray();
        System.out.println(Arrays.toString(arr));// [范冰冰, 林心如, 赵薇]
    }
}

Iterator迭代器

为什么用迭代器?

set集合没有索引,所以不能通过普通for循环遍历拿到数据,所以对于单列集合通用的遍历方式:迭代器

Iterator接口

在程序开发中,经常需要遍历单列集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator

迭代的概念

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

获取迭代器对象

Collection集合提供了一个获取迭代器的方法:

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

Iterator接口的常用方法

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。
案例演示
package com.nbchen.demo3_Iterator接口;

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

public class Test {
    public static void main(String[] args) {
        /*
            迭代:即Collection集合元素的通用获取方式。
                  在取元素之前先要判断集合中有没有元素,
                  如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。
                  一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
            获取迭代器对象: 使用Collection集合中的iterator()方法
                    public Iterator<E> iterator();

            判断集合中是否有元素可以迭代: 使用Iterator接口中的方法
                    public boolean hasNext();

            取出集合中可以迭代的元素: 使用Iterator接口中的方法
                    public E next();

         */
        // 创建Collection集合对象,限制集合中元素的类型为String
        Collection<String> col = new ArrayList<>();

        // 往col集合中添加元素
        col.add("范冰冰");
        col.add("李冰冰");
        col.add("林心如");
        col.add("赵薇");

        // 获取迭代器对象
        Iterator<String> it = col.iterator();

        // 循环判断集合中是否有元素可以迭代
        while (it.hasNext()){
            // 说明有元素可以迭代
            String e = it.next();
            System.out.println(e);
        }
    }
}

迭代器的常见问题

常见问题一

在进行集合元素获取时,如果集合中已经没有元素可以迭代了,还继续使用迭代器的next方法,将会抛出java.util.NoSuchElementException没有集合元素异常。

package com.nbchen.demo4_迭代器的常见问题;

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

public class Test1 {
    public static void main(String[] args) {
        /*
            迭代器的常见问题 :
                问题一:在进行集合元素获取时,如果集合中已经没有元素可以迭代了,还继续使用迭代器的next方法,
                       将会抛出java.util.NoSuchElementException没有集合元素异常。
         */
        // 创建Collection集合对象,限制集合中元素的类型为String
        Collection<String> col = new ArrayList<>();

        // 往col集合中添加元素
        col.add("范冰冰");
        col.add("李冰冰");
        col.add("林心如");
        col.add("赵薇");

        // 获取集合的迭代器对象
        Iterator<String> it = col.iterator();

        // 循环判断集合中是否有元素可以迭代
        while (it.hasNext()) {
            // 获取可以迭代的元素
            String e = it.next();
            System.out.println(e);
        }

        System.out.println("====================================");

        // 再获取集合中的元素
        //String next = it.next();// 运行异常NoSuchElementException
        // 如果迭代完了,还想继续迭代集合元素,就可以重新再获取一个迭代器
        Iterator<String> it2 = col.iterator();
        while (it2.hasNext()) {
            System.out.println(it2.next());
        }
    }
}

解决办法: 如果还需要重新迭代,那么就重新获取一个新的迭代器对象进行操作

常见问题二

在进行集合元素迭代时,如果添加或移除集合中的元素 , 将无法继续迭代 , 将会抛出ConcurrentModificationException并发修改异常.

package com.itheima.demo4_迭代器的常见问题;

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

/**
 * @Author:pengzhilin
 * @Date: 2020/9/13 9:46
 */
public class Test2 {
    public static void main(String[] args) {
        /*
            迭代器的常见问题:
             问题二:在进行集合元素迭代时,如果添加或移除集合中的元素 ,
                   将无法继续迭代 , 将会抛出ConcurrentModificationException并发修改异常.
         */
        // 创建Collection集合对象,限制集合中元素的类型为String
        Collection<String> col = new ArrayList<>();

        // 往col集合中添加元素
        col.add("范冰冰");
        col.add("李冰冰");
        col.add("林心如");
        col.add("赵薇");

        // 获取集合的迭代器对象
        Iterator<String> it = col.iterator();

        // 循环判断集合中是否有元素可以迭代
        while (it.hasNext()) {
            // 获取可以迭代的元素
            String e = it.next();
            System.out.println(e);
            // 添加元素到集合中
            //col.add("高圆圆");// 报异常
            // 删除元素
            //col.remove(e);// 报异常
            // 如果迭代出来的元素是李冰冰,就删除
            if (e.equals("李冰冰")){
                it.remove();
            }
        }

        System.out.println("集合:"+col);
    }
}

迭代器的实现原理

​ 我们在之前案例已经完成了Iterator遍历集合的整个过程。当遍历集合时,首先通过调用t集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。

​ Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素。在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。

image-20220709083233103

知增强for

增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作,增删操作影响指针。

其实是语法糖,idea查看out反编译class文件,可以看到实际操作。数组的增强for底层是普通for集合的增强for递增是迭代器

格式:

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

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

代码演示

package com.nbchen.demo5_增强for循环;

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

public class Test {
    public static void main(String[] args) {
        /*
            概述:增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。
                它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
            格式:
                for(元素的数据类型 变量名 :  数组名\集合名){

                }
         */
        // 创建Collection集合对象,限制集合中元素的类型为String
        Collection<String> col = new ArrayList<>();

        // 往col集合中添加元素
        col.add("范冰冰");
        col.add("李冰冰");
        col.add("林心如");
        col.add("赵薇");

        // 增强for循环遍历
        for (String e : col) {
            System.out.println(e);
        }

        System.out.println("======================================");

        String[] arr = {"范冰冰",
                "李冰冰",
                "林心如",
                "赵薇"};

        for (String e : arr){
            System.out.println(e);
        }

        System.out.println("======================================");
        // 增强for循环快捷键: 数组名\集合名.for
        for (String s : col) {
            System.out.println(s);
        }
        System.out.println("=======================================");
        for (String s : arr) {
            System.out.println(s);
        }

        System.out.println("=======================================");
        Iterator<String> it = col.iterator();
        // 迭代器快捷键: itit 回车
        while (it.hasNext()) {
            String next = it.next();
            System.out.println(next);
        }

        System.out.println("=======================================");
        // 在遍历的过程中,不能对集合中的元素进行增删操作。
        /*for (String s : col) {
            if (s.equals("李冰冰")) {
                col.remove(s);
            }
        }*/
    }
}

tips:

增强for循环必须有被遍历的目标,目标只能是Collection或者是数组;

增强for(迭代器)仅仅作为遍历操作出现,不能对集合进行增删元素操作,否则抛出ConcurrentModificationException并发修改异常

泛型

泛型的作用

集合不使用泛型的时候,存的时候什么类型都能存。但是取的时候就懵逼了。取出来啥也不是。

public class Test1 {
    public static void main(String[] args) {
        /*
            泛型的作用:
                - 集合不使用泛型: 集合不使用泛型的时候,存的时候什么类型都能存。但是取的时候就懵逼了。取出来啥也不是。
         */
        // 集合不使用泛型
        // 创建ArrayList集合对象
        ArrayList list1 = new ArrayList();

        // 往集合中添加元素
        list1.add("杨颖");
        list1.add("迪丽热巴");
        list1.add(100);
        list1.add(3.14);
        System.out.println(list1);

        // 循环遍历集合元素
        for (Object obj : list1) {
            // 在循环中,获取姓名的长度,打印输出
            String name = (String)obj;// 很容易出现类型转换异常
            System.out.println("姓名的长度:"+name.length());
        }
    }
}

使用泛型:使用泛型在编译期直接对类型作出了控制,只能存储泛型定义的数据

package com.nbchen.demo6_泛型的作用;

import java.util.ArrayList;

public class Test2 {
    public static void main(String[] args) {
        /*
            泛型的作用:
                - 集合使用泛型:使用泛型在编译期直接对类型作出了控制,只能存储泛型定义的数据
         */
        // 集合使用泛型
        // 创建ArrayList集合对象,限制集合中元素的类型为String
        ArrayList<String> list1 = new ArrayList<>();

        // 往集合中添加元素
        list1.add("杨颖");
        list1.add("迪丽热巴");
        //list1.add(100);// 编译报错
        //list1.add(3.14);// 编译报错
        System.out.println(list1);

        // 循环遍历集合元素
        for (String s : list1) {
            System.out.println(s.length());
        }
    }
}

  • 泛型:定义的时候表示一种未知的数据类型,在使用的时候确定其具体的数据类型。

tips:泛型的作用是在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

定义和使用含有泛型的类

定义含有泛型的类

定义格式:

修饰符 class 类名<代表泛型的变量> {  }
代表泛型的变量: 可以是任意字母  例如: T,E...

泛型在定义的时候不具体类型,使用的时候才具体类型。在使用的时候确定泛型的具体数据类型。

class ArrayList<E>{ 
    public boolean add(E e){ }

    public E get(int index){ }
   	....
}

确定泛型具体类型

在创建对象的时候确定泛型

例如,ArrayList<String> list = new ArrayList<String>();

此时,变量E的值就是String类型,那么我们的类型就可以理解为:

class ArrayList<String>{ 
     public boolean add(String e){ }

     public String get(int index){  }
     ...
}

课堂代码

  • 定义含有泛型的类
package com.nbchen.demo7_定义和使用含有泛型的类;

public class MyArrayList<E> {

    E e;

    public E method(E e){
        return e;
    }

}

  • 使用含有泛型的类----->掌握
package com.nbchen.demo7_定义和使用含有泛型的类;

public class Test {
    public static void main(String[] args) {
        /*
            定义含有泛型的类:
                public class 类名<泛型变量>{

                }
                泛型变量的位置: 写任意字母,例如:A,B,C,D,E,...a,b,c,...一般会写E

            使用含有泛型的类: 创建该类对象的时候,确定该类泛型的具体数据类型

            什么时候定义泛型的类:
                当类中的成员变量或者成员方法的形参类型\返回值类型不确定的时候,就可以把该类定义为含有泛型的类
         */
        MyArrayList<String> list1 = new MyArrayList<>();
        list1.e = "itheima";
        String res1 = list1.method("itcast");
        System.out.println("res1:"+res1);// itcast

        System.out.println("=======================================");

        MyArrayList<Integer> list2 = new MyArrayList<>();
        list2.e = 100;
        Integer res2 = list2.method(10);
        System.out.println("res2:"+res2);// 10

    }
}

定义和使用含有泛型的方法

修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }

例如

public class Test {
    // 定义含有泛型的方法
    public static <T> T method1(T t){
        return t;
    }
}

确定泛型具体类型

调用方法时,确定泛型的类型

public class Test {
    public static void main(String[] args) {
        /*
            定义含有泛型的方法:
                修饰符 <泛型变量> 返回值类型 方法名(形参列表){
                    方法体
                }
                泛型变量: 任意字母  一般会写T,M,...

            使用含有泛型的方法: 调用含有泛型方法的时候确定其泛型的具体数据类型
            
            什么时候会定义含有泛型的方法:
                如果一个类中,某个方法的参数类型或者返回值类型不确定的时候,可以把该方法定义为含有泛型的方法
         */
        Integer i1 = method1(100);// 指定泛型的具体数据类型为Integer
        System.out.println(i1);// 100

        System.out.println("============================");
        String s = method1("itheima");// 指定泛型的具体数据类型为String
        System.out.println(s);// itheima
    }

    // 定义含有泛型的方法
    public static <T> T method1(T t){
        return t;
    }
}

定义和使用含有泛型的接口

修饰符 interface接口名<代表泛型的变量> {  }

例如

public interface IA<E> {

    public abstract void method1(E e);

    public default E method2(E e){
        return e;
    }
}

确定泛型具体类型

使用格式:

1、定义实现类时确定泛型的类型

例如

package com.nbchen.demo09_定义和使用含有泛型的接口;

// 通过实现类的方式确定接口泛型的具体数据类型
public class Imp1 implements IA<String> {
    @Override
    public void method1(String s) {

    }

    @Override
    public String method2(String s) {
        return null;
    }
}

此时,泛型E的值就是String类型。

2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型

  • 实现类实现接口:
package com.nbchen.demo09_定义和使用含有泛型的接口;

// 实现类实现接口的时候不确定接口泛型的具体数据类型,
// 而是创建实现类对象的时候确定接口泛型的具体数据类型
public class Imp2<E> implements IA<E> {
    @Override
    public void method1(E e) {
        System.out.println("实现类 method1");
    }

    @Override
    public E method2(E e) {
        return e;
    }
}

确定泛型:

package com.nbchen.demo09_定义和使用含有泛型的接口;


public class Test {
    public static void main(String[] args) {
        /*
            定义含有泛型的接口:
                public interface 接口名<泛型变量>{

                }

                泛型变量:任意字母,一般可以使用E

            使用含有泛型的接口: 确定接口泛型的具体数据类型
                1.通过实现类的方式确定接口泛型的具体数据类型
                    public class 类名 implements 接口名<具体的数据类型>{

                    }

                2.实现类实现接口的时候不确定接口泛型的具体数据类型,
                  而是创建实现类对象的时候确定接口泛型的具体数据类型
                   public class 类名<泛型变量> implements 接口名<泛型变量>{

                    }
         */
        // 创建实现类对象的时候确定接口泛型的具体数据类型
        Imp2<String> imp1 = new Imp2<>();
        imp1.method1("itheima");
        String s1 = imp1.method2("itcast");
        System.out.println(s1);// itcast

        System.out.println("==========================");
        Imp2<Integer> imp2 = new Imp2<>();
        imp2.method1(100);
        Integer i = imp2.method2(100);
        System.out.println(i);// 100

    }
}

泛型:定义的时候表示一种未知的数据类型,在使用的时候确定其具体的数据类型。
使用含有泛型的类:  创建该类对象的时候,指定泛型的具体数据类型
使用含有方向的方法: 调用该方法的时候,确定泛型的具体数据类型
使用含有泛型的接口:
	1.创建实现类实现接口的时候,指定泛型的具体数据类型
    2.创建实现类实现接口的时候,不知道泛型的具体数据类型,而是创建实现类对象的时候指定泛型的具体数据类型

泛型通配符

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

此时只能接受数据,不能往该集合中存储数据。

例如:

public class Test {
    public static void main(String[] args) {
        /*
            通配符基本使用:
                泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。
                注意: 不能往该集合中存储数据,只能获取数据.
         */
        // 关系:String继承Object,Integer继承Number,Number继承Objec
        ArrayList<Object> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<Integer> list3 = new ArrayList<>();
        ArrayList<Number> list4 = new ArrayList<>();

        list2.add("itheima");

        //method1(list1);
        method1(list2);
        //method1(list3);
        //method1(list4);

        //method2(list1);
        method2(list2);
        //method2(list3);
        //method2(list4);

        // 泛型没有多态
        //ArrayList<Object> list = new ArrayList<String>();// 编译报错
    }
    // 定义一个方法,可以接收以上4个集合
    public static void method1(ArrayList list){
        Object obj = list.get(0);
        list.add("jack");
        System.out.println("obj:"+obj);// itheima
        System.out.println("list:"+list);// [itheima, jack]
    }

    public static void method2(ArrayList<?> list){
        Object obj = list.get(0);
        //list.add("jack");// 编译报错
        System.out.println("obj:"+obj);// itheima
        System.out.println("list:"+list);// [itheima]
    }
}

通配符高级使用----受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

泛型的上限

  • 格式类型名称 <? extends 类 > 对象名称
  • 意义只能接收该类型及其子类

泛型的下限

  • 格式类型名称 <? super 类 > 对象名称
  • 意义只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

public class Test {
    public static void main(String[] args) {
        /*
            通配符高级使用----受限泛型:
                上限: <? extends 类名>  只能接收该类类型或者其子类类型
                下限: <? super 类名>   只能接收该类类型或者其父类类型
         */
        // 关系:String继承Object,Integer继承Number,Number继承Objec
        ArrayList<Object> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<Integer> list3 = new ArrayList<>();
        ArrayList<Number> list4 = new ArrayList<>();
        
        method1(list1);
        method1(list2);
        method1(list3);
        method1(list4);
        
        //method2(list1);// 编译报错
        //method2(list2);// 编译报错
        method2(list3);
        method2(list4);


        method3(list1);
        //method3(list2);// 编译报错
        method3(list3);
        method3(list4);
    }

    // 定义一个方法,只可以接收以上list3和list4集合
    public static void method2(ArrayList<? extends Number> list){

    }
    
    // 定义一个方法,只可以接收以上list3和list4,list1集合
    public static void method3(ArrayList<? super Integer> list){

    }
    
    // 定义一个方法,可以接收以上4个集合
    public static void method1(ArrayList<?> list){
       
    }
    // 定义一个方法,可以接收以上4个集合
    public static void method(ArrayList list){

    }
}

数据结构

数据结构介绍

数据结构 : 其实就是存储数据和表示数据的方式。数据结构内容比较多,细细的学起来也是相对费功夫的,不可能达到一蹴而就。我们将常见的数据结构:堆栈、队列、数组、链表和红黑树 这几种给大家介绍一下,作为数据结构的入门,了解一下它们的特点即可。

常见数据结构

数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下:

  • 栈(stack,堆栈):压栈弹栈,先进后出,子弹压膛发射。
  • 队列(queue):先进先出,火车过山洞。
  • 数组:查询快,增删慢。
    • 查询[快]:连续的空间,有索引,存的是首地址,如int[],首地址0x001,后续只要首地址+索引*4个字节。
    • 增加[慢]:长度固定不变,就得新建数组,复制,每个值地址都会改变。
    • 删除[慢],新建数组或者后面数据整体前移,后面的数据值地址都会改变。
  • 链表(单/双向链表):单:手拉手,查询慢,增删快。
    • 查询[慢]:数据是离散的,必须一个一个找。
    • 增[快]:因为链表中的要新增数据或者减少数据,只要开一个新的空间,将2个节点的地址部分数据修改一次即可,其他链表中的所有数据不用做改变。
  • 红黑树(二叉查找平衡树):查询增删速度特别快,效率高。
    • 二叉树:最多两个子节点的数
    • 查询树或者排序树:二叉树的基础上,满足节点一定符合左小右大的特点。
    • 平衡树,左右的节点相近。

stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在表的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。

简单的说:采用该结构的集合,对元素的存取有如下的特点

  • 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。

  • 栈的入口、出口的都是栈的顶端位置

image-20220709084310262

这里两个名词需要注意:

  • 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
  • 弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。

队列

队列queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行取出并删除。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
  • 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。

image-20220709084327278

数组

数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 查找元素快:通过索引,可以快速访问指定位置的元素

image-20220709084356822

  • 增删元素慢

  • 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。如下图

image-20220709084413688

  • 指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图

image-20220709084429483

链表

链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表

image-20220709084500357

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。

  • 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素。

  • 增删元素快:只需要修改链接下一个元素的地址值即可

image-20220709084531275

树基本结构介绍

树具有的特点

  1. 每一个节点有零个或者多个子节点
  2. 没有父节点的节点称之为根节点,一个树最多有一个根节点。
  3. 每一个非根节点有且只有一个父节点

image-20220709084619569

名词 含义
节点 指树中的一个元素
节点的度 节点拥有的子树的个数,二叉树的度不大于2
叶子节点 度为0的节点,也称之为终端结点
高度 叶子结点的高度为1,叶子结点的父节点高度为2,以此类推,根节点的高度最高
根节点在第一层,以此类推
父节点 若一个节点含有子节点,则这个节点称之为其子节点的父节点
子节点 子节点是父节点的下一层节点
兄弟节点 拥有共同父节点的节点互称为兄弟节点

二叉树

如果树中的每个节点的子节点的个数不超过2,那么该树就是一个二叉树。

image-20220709084709435

二叉查找树

二叉查找树的特点:

  1. 左子树上所有的节点的值均小于等于他的根节点的值
  2. 右子树上所有的节点值均大于或者等于他的根节点的值
  3. 每一个子节点最多有两个子树

案例演示(20,18,23,22,17,24,19)数据的存储过程;

image-20220709084730549

遍历获取元素的时候可以按照"左中右"的顺序进行遍历;

注意:二叉查找树存在的问题:会出现"瘸子"的现象,影响查询效率

image-20220709084751591

平衡二叉树

为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:"平衡二叉树"

规则:它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

如下图所示:

image-20220709084822547

如下图所示,左图是一棵平衡二叉树,根节点10,左右两子树的高度差是1,而右图,虽然根节点左右两子树高度差是0,但是右子树15的左右子树高度差为2,不符合定义,

所以右图不是一棵平衡二叉树。

旋转

在构建一棵平衡二叉树的过程中,当有新的节点要插入时,检查是否因插入后而破坏了树的平衡,如果是,则需要做旋转去改变树的结构。

左旋

左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点;

image-20220709084906598

右旋

将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点

image-20220709084851644

举个例子,像上图是否平衡二叉树的图里面,左图在没插入前"19"节点前,该树还是平衡二叉树,但是在插入"19"后,导致了"15"的左右子树失去了"平衡",

所以此时可以将"15"节点进行左旋,让"15"自身把节点出让给"17"作为"17"的左树,使得"17"节点左右子树平衡,而"15"节点没有子树,左右也平衡了。如下图,

image-20220709084927090

由于在构建平衡二叉树的时候,当有新节点插入时,都会判断插入后时候平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。当新节点插入后,

有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作左左,左右,右左,右右

左左:只需要做一次右旋就变成了平衡二叉树。
右右:只需要做一次左旋就变成了平衡二叉树。
左右:先做一次分支的左旋,再做一次树的右旋,才能变成平衡二叉树。
右左:先做一次分支的右旋,再做一次数的左旋,才能变成平衡二叉树。
课上只讲解“左左”的情况
左左

左左:只需要做一次右旋就变成了平衡二叉树。

左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"10"节点的左子树"7",的左子树"4",插入了节点"5"或"3"导致失衡。

image-20220709084949709

左左调整其实比较简单,只需要对节点进行右旋即可,如下图,对节点"10"进行右旋,

image-20220709085008819

image-20220709085033779

左右

左右:先做一次分支的左旋,再做一次树的右旋,才能变成平衡二叉树。

左右即为在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的左子树"7",的右子树"9",

插入了节点"10"或"8"导致失衡。

image-20220709085107141

左右的调整就不能像左左一样,进行一次旋转就完成调整。我们不妨先试着让左右像左左一样对"11"节点进行右旋,结果图如下,右图的二叉树依然不平衡,而右图就是接下来要

讲的右左,即左右跟右左互为镜像,左左跟右右也互为镜像。

image-20220709085126785

左右这种情况,进行一次旋转是不能满足我们的条件的,正确的调整方式是,将左右进行第一次旋转,将左右先调整成左左,然后再对左左进行调整,从而使得二叉树平衡。

即先对上图的节点"7"进行左旋,使得二叉树变成了左左,之后再对"11"节点进行右旋,此时二叉树就调整完成,如下图:

image-20220709085143156

右左

右左:先做一次分支的右旋,再做一次数的左旋,才能变成平衡二叉树。

右左即为在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的右子树"15",的左子树"13",

插入了节点"12"或"14"导致失衡。

image-20220709085201026

前面也说了,右左跟左右其实互为镜像,所以调整过程就反过来,先对节点"15"进行右旋,使得二叉树变成右右,之后再对"11"节点进行左旋,此时二叉树就调整完成,如下图:

image-20220709085228294

右右

右右:只需要做一次左旋就变成了平衡二叉树。

右右即为在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"11"节点的右子树"13",的左子树"15",插入了节点

"14"或"19"导致失衡。

image-20220709085252083

右右只需对节点进行一次左旋即可调整平衡,如下图,对"11"节点进行左旋。

image-20220709085310613

红黑树

红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构,它是在1972年由Rudolf Bayer发明的,当时被称之为平衡二叉B树,后来,在1978年被

Leoj.Guibas和Robert Sedgewick修改为如今的"红黑树"。它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色,可以是红或者黑;

红黑树不是高度平衡的,它的平衡是通过"红黑树的特性"进行实现的;

红黑树的特性:

  1. 每一个节点或是红色的,或者是黑色的。
  2. 根节点必须是黑色
  3. 每个叶节点(Nil)是黑色的;(如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点)
  4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
  5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点;

如下图所示就是一个

image-20220709085328654

在进行元素插入的时候,和之前一样; 每一次插入完毕以后,使用黑色规则进行校验,如果不满足红黑规则,就需要通过变色,左旋和右旋来调整树,使其满足红黑规则;

小结

  • 红黑树的作用: 提高搜索效率
  • 表示集合的类有很多,但是每个集合存储数据的的数据结构不同,所以每个集合有各自的特点,
  • ArrayList集合: 查询快,增删慢 --->存储数据的数据结构是数组
  • LinkedList集合: 查询慢,增删快--->存储数据的数据结构是链表
  • .....

List接口

List接口介绍

我们掌握了Collection接口的使用后,再来看看Collection接口中的子类,他们都具备那些特性呢?

接下来,我们一起学习Collection中的常用几个子类(java.util.List集合、java.util.Set集合)。

List接口的概述

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

List接口特点

  1. 它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  2. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  3. 集合中可以有重复的元素。

tips:我们在基础班的时候已经学习过List接口的子类java.util.ArrayList类,该类中的方法都是来自List中定义。

List接口中常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法

List接口新增常用方法

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

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index):返回集合中指定位置的元素。
  • public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

List集合特有的方法都是跟索引相关,我们在基础班都学习过。

List接口新增常用方法的使用

public class Test {
    public static void main(String[] args) {
        /*
            List接口新增常用方法:
                - public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
                - public E get(int index):返回集合中指定位置的元素。
                - public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。
                - public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素
         */
        // 创建list集合,限制集合中元素的类型为String类型
        List<String> list = new ArrayList<>();

        // 往集合中添加一些元素
        list.add("苍老师");
        list.add("波老师");
        list.add("吉泽老师");
        System.out.println(list);// [苍老师, 波老师, 吉泽老师]

        // 在索引为1的位置添加小泽老师
        list.add(1, "小泽老师");
        System.out.println(list);// [苍老师, 小泽老师, 波老师, 吉泽老师]

        // 获取索引为1的元素
        System.out.println("索引为1的元素:"+list.get(1));// 小泽老师

        // 删除索引为1的老师
        String removeE = list.remove(1);
        System.out.println("被删除的元素:"+removeE);// 小泽老师
        System.out.println(list);//  [苍老师, 波老师, 吉泽老师]

        // 把索引为0的元素替换为大桥老师
        String setE = list.set(0, "大桥老师");
        System.out.println("被替换的元素:"+setE);// 苍老师
        System.out.println(list);// [大桥老师, 波老师, 吉泽老师]
    }
}

List的子类

ArrayList集合

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

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

原理:底层实现是Object[]数组

  • 存储
Transient Object[] elementData;

只能存储Object类型或者他的子类,引用类型只能存引用类型(除了包装类自动拆箱)。所以,ArrayList存不了int基本类型。ArrayList<int>是错的

  • 删除
System.arraycopy(es,i+1,es,i,newSize-i);

将数组后面的数组往前移动一位,进行覆盖

ArrayList源码:

1.为什么ArrayList源码中要用成员内部类表示迭代器的实现子类?从调用的时候封装具体内容方面理解这种定义的好处吗?
内部类,因为只在这个类里面用,定义在外面别的类也用不到,就更private封装类似。

调用:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
Collection<String> coll=newArrayList<>();
Iterator<String> it=coll.iterator();

2.为什么要复制给局部变量,直接操作有什么弊端?

Object[]elementData=ArrayList.this.elementData;

这里存的是地址值,直接用也是可以的

LinkedList集合

java.util.LinkedList集合数据存储的结构是链表结构。方便元素添加、删除的集合。查询慢,增删快

LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下

image-20220709085631208

实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可

  • public void addFirst(E e):将指定元素插入此列表的开头。
  • public void addLast(E e):将指定元素添加到此列表的结尾。
  • public E getFirst():返回此列表的第一个元素。
  • public E getLast():返回此列表的最后一个元素。
  • public E removeFirst():移除并返回此列表的第一个元素。
  • public E removeLast():移除并返回此列表的最后一个元素。
  • public E pop():从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e):将元素推入此列表所表示的堆栈。

Pop(弹栈)和push(压栈)就是调用的removeFirst和addFirst

LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。

public class Test {
    public static void main(String[] args) {
        /*
            LinkedList集合特有的方法:
                - public void addFirst(E e):将指定元素插入此列表的开头。
                - public void addLast(E e):将指定元素添加到此列表的结尾。
                - public E getFirst():返回此列表的第一个元素。
                - public E getLast():返回此列表的最后一个元素。
                - public E removeFirst():移除并返回此列表的第一个元素。
                - public E removeLast():移除并返回此列表的最后一个元素。
                - public E pop():从此列表所表示的堆栈处弹出一个元素。 removeFirst()
                - public void push(E e):将元素推入此列表所表示的堆栈。addFirst()
         */
        // 创建LinkedList集合,限制集合元素的类型为String类型
        LinkedList<String> list = new LinkedList<>();

        // 往集合中添加元素
        list.add("蔡徐坤");
        list.add("鹿晗");
        list.add("吴亦凡");
        System.out.println(list);// [蔡徐坤, 鹿晗, 吴亦凡]

        // 在集合的首尾添加一个元素
        list.addFirst("罗志祥");
        list.addLast("陈冠希");
        System.out.println(list);//  [罗志祥, 蔡徐坤, 鹿晗, 吴亦凡, 陈冠希]

        // 获取集合的首尾元素
        String firstE = list.getFirst();
        String lastE = list.getLast();
        System.out.println("第一个元素是:"+firstE);// 罗志祥
        System.out.println("最后一个元素是:"+lastE);// 陈冠希

        // 删除首尾元素
        String removeFirst = list.removeFirst();
        String removeLast = list.removeLast();
        System.out.println("被删除的第一个元素是:"+removeFirst);// 罗志祥
        System.out.println("被删除的最后一个元素是:"+removeLast);// 陈冠希
        System.out.println(list);//  [蔡徐坤, 鹿晗, 吴亦凡]

        // pop  --->删除第一个元素
        String popE = list.pop();
        System.out.println("被删除的第一个元素是:"+popE);// 蔡徐坤
        System.out.println(list);// [鹿晗, 吴亦凡]

        // push --->添加一个元素在开头
        list.push("蔡徐坤");
        System.out.println(list); // [蔡徐坤, 鹿晗, 吴亦凡]
    }
}

集合综合案例

按照斗地主的规则,完成造牌洗牌发牌的动作。
具体规则:

使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。

分析

  • 准备牌:

    牌可以设计为一个ArrayList,每个字符串为一张牌。
    每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。
    牌由Collections类的shuffle方法进行随机排序。

  • 发牌

    将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。

  • 看牌

    直接打印每个集合。

实现

import java.util.ArrayList;
import java.util.Collections;

public class Test {
    public static void main(String[] args) {
        // 1.造牌:
        // 1.1 创建一个pokerBox集合,用来存储54张扑克牌
        ArrayList<String> pokerBox = new ArrayList<>();

        // 1.2 创建一个ArrayList牌面值集合,用来存储13个牌面值
        ArrayList<String> numbers = new ArrayList<>();

        // 1.3 创建一个ArrayList花色集合,用来存储4个花色
        ArrayList<String> colors = new ArrayList<>();

        // 1.4 往牌面值集合中添加13个牌面值
        numbers.add("A");
        numbers.add("K");
        numbers.add("Q");
        numbers.add("J");
        for (int i = 2; i <= 10; i++) {
            numbers.add(i + "");
        }

        // 1.5 往花色集合中添加4个花色
        colors.add("♥");
        colors.add("♠");
        colors.add("♣");
        colors.add("♦");

        // 1.6 添加大小王到存储到pokerBox集合中
        pokerBox.add("大王");
        pokerBox.add("小王");

        // 1.7 花色集合和牌面值集合,循环嵌套
        for (String number : numbers) {
            for (String color : colors) {
                // 1.8 在循环里面创建牌,并添加到pokerBox集合中
                String pai = color + number;
                pokerBox.add(pai);
            }
        }
        // 1.9 打印pokerBox集合
        System.out.println(pokerBox);
        System.out.println(pokerBox.size());

        // 2.洗牌:
        // 使用Collections工具类的静态方法
        // public static void shuffle(List<?> list)
        // 打乱集合元素的顺序
        Collections.shuffle(pokerBox);
        System.out.println("打乱顺序后:" + pokerBox);
        System.out.println("打乱顺序后:" + pokerBox.size());

        // 3.发牌
        // 3.1 创建4个ArrayList集合,分别用来存储玩家1,玩家2,玩家3,底牌的牌
        ArrayList<String> play1 = new ArrayList<>();
        ArrayList<String> play2 = new ArrayList<>();
        ArrayList<String> play3 = new ArrayList<>();
        ArrayList<String> diPai = new ArrayList<>();

        // 3.2 循环遍历打乱顺序之后的牌
        for (int i = 0; i < pokerBox.size(); i++) {
            // 3.3 在循环中,获取遍历出来的牌
            String pai = pokerBox.get(i);
            // 3.4 在循环中,判断遍历出来的牌:
            if (i >= 51) {
                // 3.5 如果该牌的索引是51,52,53,给底牌
                diPai.add(pai);
            } else if (i % 3 == 0) {
                // 3.5 如果该牌的索引%3==0,给玩家1
                play1.add(pai);
            } else if (i % 3 == 1) {
                // 3.5 如果该牌的索引%3==1,给玩家2
                play2.add(pai);
            } else if (i % 3 == 2) {
                // 3.5 如果该牌的索引%3==2,给玩家3
                play3.add(pai);
            }
        }
        // 3.6 打印各自的牌
        System.out.println("玩家1:"+play1+",牌数:"+play1.size());
        System.out.println("玩家2:"+play2+",牌数:"+play2.size());
        System.out.println("玩家3:"+play3+",牌数:"+play3.size());
        System.out.println("底牌:"+diPai);
    }
}

Collections类

Collections常用功能

  • java.utils.Collections是集合工具类,用来对集合进行操作。

    常用方法如下:

  • public static void shuffle(List<?> list) :打乱集合顺序。

  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

自定义规则:

1.对象必须实现Comparable接口:默认排序,较死板
2.排序时实现comparator比较器:compare(String o1,String o2);

升序:我-它
	o1 < o2,返回负数,相等返回0,o1 > o2返回正数

降序:
	它-我o1 < o2,返回正数,相等返回0,o1 > o2返回负数
  • shuffle方法代码演示:
public class Test1_shuffle {
    public static void main(String[] args) {
        /*
            Collections常用功能:
                public static void shuffle(List<?> list):打乱集合顺序。
         */
        // 创建List集合,限制集合中元素的类型为Integer类型
        List<Integer> list = new ArrayList<>();

        // 往集合中添加元素
        list.add(300);
        list.add(100);
        list.add(200);
        list.add(500);
        list.add(400);

        System.out.println("打乱顺序之前的集合:"+list);// [300, 100, 200, 500, 400]
        // 打乱顺序
        Collections.shuffle(list); // 随机打乱顺序
        System.out.println("打乱顺序之后的集合:"+list);// [500, 300, 100, 200, 400]

    }
}

  • sort方法代码演示:按照默认规则排序
public class Student implements Comparable<Student>{
    int age;

    public Student(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        // 指定排序规则
        // 前减后  升序
        // 后减前  降序
        // 前:this  后: 参数o
        return this.age - o.age;// 升序
    }
}



public class Test2_sort {
    public static void main(String[] args) {
        /*
            Collections常用功能:
                public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
                默认规则: 事先写好的规则
                排序规则: 集合元素所属的类一定要实现Comparable接口,重写compareTo方法,在compareTo方法中指定排序规则
         */
        // 创建List集合,限制集合中元素的类型为Integer类型
        List<Integer> list = new ArrayList<>();

        // 往集合中添加元素
        list.add(300);
        list.add(100);
        list.add(200);
        list.add(500);
        list.add(400);

        System.out.println("排序之前的集合:"+list); // [300, 100, 200, 500, 400]
        // 将集合中元素按照默认规则排序
        Collections.sort(list);
        System.out.println("排序之后的集合:"+list); // [100, 200, 300, 400, 500]

        System.out.println("=========================================");

        // 创建List集合,限制集合中元素的类型为Student类型
        List<Student> list1 = new ArrayList<>();

        // 往集合中添加元素
        Student stu1 = new Student(19);
        Student stu2 = new Student(18);
        Student stu3 = new Student(20);
        Student stu4 = new Student(17);
        list1.add(stu1);
        list1.add(stu2);
        list1.add(stu3);
        list1.add(stu4);
        System.out.println("排序之前的集合:"+list1);
        // 将集合中元素按照默认规则排序
        Collections.sort(list1);
        System.out.println("排序之后的集合:"+list1);


    }
}

  • sort方法代码块演示: 指定规则排序

public class Test3_sort {
    public static void main(String[] args) {
        /*
            Collections常用功能:
             public static <T> void sort(List<T> list,Comparator<? super T> com):将集合中元素按照指定规则排序。
              参数Comparator: 就是用来指定排序规则的
              通过Comparator接口中的compare方法来指定排序规则
         */
        // 创建List集合,限制集合中元素的类型为Integer类型
        List<Integer> list = new ArrayList<>();

        // 往集合中添加元素
        list.add(300);
        list.add(100);
        list.add(200);
        list.add(500);
        list.add(400);

        System.out.println("排序之前的集合:" + list); // [300, 100, 200, 500, 400]
        // 将集合中元素按照指定规则排序---->降序
        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                // 指定排序规则
                // 前减后  升序
                // 后减前  降序
                // 前: 第一个参数o1  后:第二个参数o2
                return o2 - o1;
            }
        });
        System.out.println("排序之后的集合:" + list); // [500, 400, 300, 200, 100]

        // 将集合中元素按照指定规则排序---->升序
        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });
        System.out.println("排序之后的集合:" + list);// [100, 200, 300, 400, 500]

        System.out.println("=======================================");

        // 创建List集合,限制集合中元素的类型为Student类型
        List<Student> list1 = new ArrayList<>();

        // 往集合中添加元素
        Student stu1 = new Student(19);
        Student stu2 = new Student(18);
        Student stu3 = new Student(20);
        Student stu4 = new Student(17);
        list1.add(stu1);
        list1.add(stu2);
        list1.add(stu3);
        list1.add(stu4);
        System.out.println("排序之前的集合:" + list1);
        // 将集合中元素按照指定规则排序-->按照年龄降序排序
        Collections.sort(list1, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 指定排序规则
                // 前减后  升序
                // 后减前  降序
                // 前: 第一个参数o1  后:第二个参数o2
                return o2.age - o1.age;
            }
        });
        System.out.println("排序之后的集合:" + list1);

        // 将集合中元素按照指定规则排序-->按照年龄升序排序
        Collections.sort(list1, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 指定排序规则
                // 前减后  升序
                // 后减前  降序
                // 前: 第一个参数o1  后:第二个参数o2
                return o1.age - o2.age;
            }
        });
        System.out.println("排序之后的集合:" + list1);
    }
}

小结

- public static void shuffle(List<?> list):打乱集合顺序。
- public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
    默认规则: 事先写好的排序规则
    在哪里写好排序规则?---->集合元素所属的类中写好排序规则(通过实现Comparable接口,重写compareTo(T o)方法写好排序规则)
     排序规则: 
		前减后  升序 
    	后减前  降序
    	前: this   后:参数  
            
- public static <T> void sort(List<T> list,Comparator<? super T>  com):将集合中元素按照指定规则排序。
   指定规则排序:  通过Comparator参数来指定
   通过传入Comparator接口的匿名内部类,重写compare(T o1,T o2)方法,在该方法中指定排序规则      
      排序规则: 
		前减后  升序 
    	后减前  降序
    	前: 第一个参数   后:第二个参数    

可变参数

可变参数的使用

JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.

格式:

修饰符 返回值类型 方法名(参数类型... 形参名){  }

代码演示:


public class Test1 {
    public static void main(String[] args) {
        /*
            可变参数:
                概述:在JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.
                格式:
                    修饰符 返回值类型 方法名(数据类型... 变量名){}

         */
       /* method1(10,20,30,40,50);

        int[] arr = {10,20,30,40,50};
        method2(arr);*/

        /*method3();
        method3(10,20,30,40);*/
        method3(10,20,30,40,50);

       /* int[] arr = {10,20,30,40,50};
        method3(arr);*/
    }

    // 定义一个方法,可以接收5个int类型的数
    public static void method3(int... nums){
        // 使用:把nums可变参数当成数组使用
        for (int i = 0; i < nums.length; i++) {
            System.out.println(nums[i]);
        }
    }

    // 定义一个方法,可以接收5个int类型的数
    public static void method2(int[] arr){// 接收数组
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }

    // 定义一个方法,可以接收5个int类型的数
    public static void method1(int num1,int num2,int num3,int num4,int num5){// 接收5个具体的数

    }

}

注意事项

​ 1.一个方法只能有一个可变参数

​ 2.如果方法中有多个参数,可变参数要放到最后。

package com.itheima.demo2_可变参数;

/**
 * @Author:pengzhilin
 * @Date: 2020/9/15 9:55
 */
public class Test2 {
    public static void main(String[] args) {
        /*
            可变注意事项:
                    1.一个方法,只能有一个可变参数
                    2.如果方法有多个参数,可变参数一定要放在末尾
         */
        // method1(10,20,"itheima");
        // method2(10,20,"itheima");
         method3("itheima",10,20);
    }

    // 编译报错,因为一个方法,只能有一个可变参数
    /*public static void method1(int... nums,String... strs){

    }*/

    // 编译报错,因为如果方法有多个参数,可变参数一定要放在末尾
    /*public static void method2(int... nums,String  str){

    }*/

    public static void method3(String  str,int... nums){

    }
}

应用场景: Collections

​ 在Collections中也提供了添加一些元素方法:

public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。

代码演示:


public class Test3 {
    public static void main(String[] args) {
        /*
            应用场景: Collections
                在Collections中也提供了添加一些元素方法:
                public static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加一些元素。
         */
        // 创建ArrayList集合,限制集合元素的类型为String类型
        ArrayList<String> list = new ArrayList<>();

        // 往list集合中添加批量元素
        Collections.addAll(list,"2","A","K","Q","J","10","9","8","7","6","5","4","3");
        System.out.println(list);
    }
}

Set接口

Set接口介绍

 Set接口:也称Set集合,但凡是实现了Set接口的类都叫做Set集合
	特点: 元素无索引,元素不可重复(唯一)
    HashSet集合: 实现类--元素存取无序
    LinkedHashSet集合:实现类--元素存取有序
    TreeSet集合:实现类--> 对元素进行排序
 注意:
	1.Set集合没有特殊的方法,都是使用Collection接口的方法
    2.Set集合没有索引,所以遍历元素的方式就只有: 增强for循环,或者迭代器

HashSet集合

java.util.HashSetSet接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不能保证不一致)。

底层实现是hashMap,采用的数据结构是哈希表结构,此结构特点:快

我们先来使用一下Set集合存储,看下现象,再进行原理的讲解:

public class Test {
    public static void main(String[] args) {
        /*
            HashSet集合: 元素存取无序,元素不可重复,元素无索引
         */
        // 创建HashSet集合对象,限制集合元素的类型为String
        HashSet<String> set = new HashSet<>();

        // 往集合中添加元素
        set.add("nba");
        set.add("cba");
        set.add("bac");
        set.add("abc");
        set.add("nba");

        System.out.println(set);// [cba, abc, bac, nba]
    }
}

HashSet集合存储数据的结构(哈希表)

对象的哈希值

就是一个十进制的值,它就是一个对象的特征码(返回值int,-21亿~+21亿,哈希值有限,万物皆对象,hash值有可能重复!{通话和重地的hashCode一样})

Object的toString返回的地址值是hashCode(十进制)的十六进制表示形式。
Object,hashCode因为存的是真正的物理地址值,所以基本上不太可能重复,equals用==比较。

每个对象的特征码应该与该对象的内容有关,父类的hashCode不满足的需求:重写hashCode和equals

哈希表底层结构

JDK1.8之前,哈希表底层采用数组+链表实现,即使用数组处理冲突,同一hash值的链表都存储在一个数组里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

1587659262084

HashSet保证元素唯一原理

hashCode+equals

hashCode不一样,对象肯定不一样
hashCode一样,equals不一样,对象不一样
hashCode一样,equals一样,对象一样
为啥不直接调用equals判断两个对象是否一样,而要先掉hashCode?
哈希加速,提高运算效率
这就是为什么用到set集合的时候必须重写hashCode和equals

-HashSet集合存储数据的结构---哈希表结构
    哈希表结构:
          jdk8以前: 数组+链表
          jdk8以后: 数组+链表+红黑树
                链表元素个数没有超过8: 数组+链表
                链表元素个数超过8: 数组+链表+红黑树

-HashSet集合保证元素唯一的原理--依赖hashCode()和equals()方法
    1.当存储元素的时候,就会调用该元素的hashCode()方法计算该元素的哈希值
    2.判断该哈希值对应的位置上,是否有元素:
    3.如果该哈希值对应的位置上,没有元素,就直接存储
    4.如果该哈希值对应的位置上,有元素,说明产生了哈希冲突
    5.产生了哈希冲突,就得调用该元素的equals方法,与该位置上的所有元素进行一一比较:
       如果比较的时候,有任意一个元素与该元素相同,那么就不存储
       如果比较完了,没有一个元素与该元素相同,那么就直接存储

补充:
     Object类: hashCode()和equals()方法;
              hashCode():Object类中的hashCode()方法是根据地址值计算哈希值
              equals方法():Object类中的equals()方法是比较地址值
public class Demo {
    public static void main(String[] args) {
        // 创建一个HashSet集合,限制集合中元素的类型为String
        HashSet<String> set = new HashSet<>();

        // 往集合中添加一些元素
        set.add("nba");
        set.add("cba");
        set.add("bac");
        set.add("abc");
        set.add("nba");

        // 遍历打印集合
        for (String e : set) {
            System.out.println(e);// cba abc  bac  nba
        }

        System.out.println("nba".hashCode());// nba:108845
        System.out.println("cba".hashCode());// cba:98274
        System.out.println("bac".hashCode());// bac:97284
        System.out.println("abc".hashCode());// abc:96354
    }
}

image-20200915104218936

HashSet的源码分析

HashSet的成员属性及构造方法

public class HashSet<E> extends AbstractSet<E>
    					implements Set<E>, Cloneable, java.io.Serializable{
    
	//内部一个HashMap——HashSet内部实际上是用HashMap实现的
    private transient HashMap<E,Object> map;
    // 用于做map的值
    private static final Object PRESENT = new Object();
    /**
     * 构造一个新的HashSet,
     * 内部实际上是构造了一个HashMap
     */
    public HashSet() {
        map = new HashMap<>();
    }
    
}
  • 通过构造方法可以看出,HashSet构造时,实际上是构造一个HashMap

HashSet的add方法源码解析

public class HashSet{
    //......
    public boolean add(E e) {
       return map.put(e, PRESENT)==null;//内部实际上添加到map中,键:要添加的对象,值:Object对象
    }
    //......
}

HashMap的put方法源码解析

public class HashMap{
    //......
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //......
    static final int hash(Object key) {//根据参数,产生一个哈希值
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //......
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; //临时变量,存储"哈希表"——由此可见,哈希表是一个Node[]数组
        Node<K,V> p;//临时变量,用于存储从"哈希表"中获取的Node
        int n, i;//n存储哈希表长度;i存储哈希表索引
        
        if ((tab = table) == null || (n = tab.length) == 0)//判断当前是否还没有生成哈希表
            n = (tab = resize()).length;//resize()方法用于生成一个哈希表,默认长度:16,赋给n
        if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash等效于hash % n,转换为数组索引
            tab[i] = newNode(hash, key, value, null);//此位置没有元素,直接存储
        else {//否则此位置已经有元素了
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//判断哈希值和equals
                e = p;//将哈希表中的元素存储为e
            else if (p instanceof TreeNode)//判断是否为"树"结构
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//排除以上两种情况,将其存为新的Node节点
                for (int binCount = 0; ; ++binCount) {//遍历链表
                    if ((e = p.next) == null) {//找到最后一个节点
                        p.next = newNode(hash, key, value, null);//产生一个新节点,赋值到链表
                        if (binCount >= TREEIFY_THRESHOLD - 1) //判断链表长度是否大于了8
                            treeifyBin(tab, hash);//树形化
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//跟当前变量的元素比较,如果hashCode相同,equals也相同
                        break;//结束循环
                    p = e;//将p设为当前遍历的Node节点
                }
            }
            if (e != null) { // 如果存在此键
                V oldValue = e.value;//取出value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//设置为新value
                afterNodeAccess(e);//空方法,什么都不做
                return oldValue;//返回旧值
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
}

HashSet存储自定义类型元素

给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一.

public class Person{
    /**
     * 姓名
     */
    public String name;
    /**
     * 年龄
     */
    public int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

创建测试类:

public class Demo {
    public static void main(String[] args) {
        // 创建多个Person对象
        Person p1 = new Person("张三", 18);
        Person p2 = new Person("李四", 38);
        Person p3 = new Person("王五", 28);
        Person p4 = new Person("张三", 18);

        // 创建HashSet集合对象,限制集合中元素的类型为Person
        HashSet<Person> set = new HashSet<>();

        // 往集合中添加Person对象
        set.add(p1);
        set.add(p2);
        set.add(p3);
        set.add(p4);

        // 遍历打印集合中的元素
        for (Person p : set) {
            System.out.println(p);
        }

        System.out.println(p1.hashCode());
        System.out.println(p2.hashCode());
        System.out.println(p3.hashCode());
        System.out.println(p4.hashCode());
    }
}

LinkedHashSet

我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?

在HashSet下面有一个子类java.util.LinkedHashSet速度很快,而且能够保证顺序,它是链表(有序)+哈希表(去重)组合的一个数据存储结构。

演示代码如下:


import java.util.HashSet;
import java.util.LinkedHashSet;

/**
 * @Author:pengzhilin
 * @Date: 2020/9/15 11:34
 */
public class Test {
    public static void main(String[] args) {
        /*
            LinkedHashSet集合: 元素存取有序,元素无索引,元素不可重复(唯一)
                采用哈希表+链表结构,由哈希表保证元素唯一,由链表保证元素存取有序
         */
        // 创建LinkedHashSet集合,限制集合中元素的类型为Integer类型
        LinkedHashSet<Integer> set = new LinkedHashSet<>();// 存取有序
        //HashSet<Integer> set = new HashSet<>();// 存取无序

        // 往集合中存储数据
        set.add(300);
        set.add(100);
        set.add(200);
        set.add(500);
        set.add(400);
        set.add(400);

        System.out.println(set);// [300, 100, 200, 500, 400]
    }
}

TreeSet集合

TreeSet集合是Set接口的一个实现类,底层依赖于TreeMap,是一种基于红黑树的实现,其特点为:

  1. 元素唯一
  2. 元素没有索引
  3. 使用元素的自然顺序对元素进行排序,或者根据创建 TreeSet 时提供的 Comparator 比较器
    进行排序,具体取决于使用的构造方法:
public TreeSet():								根据其元素的自然排序进行排序
public TreeSet(Comparator<E> comparator):    根据指定的比较器进行排序

案例



import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {
        /*
            TreeSet集合: 元素无索引,元素唯一,对元素进行排序
                通过构造方法实现排序:
                    public TreeSet():						     根据其元素的自然排序进行排序
                            默认规则排序:集合元素所属的类需要实现Comparable接口,重写compareTo方法,在compareTo方法中指定默认排序规则

                    public TreeSet(Comparator<E> comparator):    根据指定的比较器进行排序
                            指定规则排序: 通过传入Comparator接口的实现类对象,在实现类对象中重写compare方法,在compare方法中指定排序规则
         */
        // 按照默认规则排序---->默认升序
        // 创建TreeSet集合,限制集合中元素的类型为Integer类型
        TreeSet<Integer> set = new TreeSet<>();

        // 往集合中存储数据
        set.add(300);
        set.add(100);
        set.add(200);
        set.add(500);
        set.add(400);
        set.add(400);
        System.out.println(set);// [100, 200, 300, 400, 500]

        System.out.println("===========================================");
        // 按照指定规则排序---->降序
        // 创建TreeSet集合,限制集合中元素的类型为Integer类型
        TreeSet<Integer> set1 = new TreeSet<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                /*
                    指定排序规则:
                    前减后  升序
                    后减前  降序
                    前:第一个参数  后:第二个参数
                 */
                return o2 - o1;
            }
        });

        // 往集合中存储数据
        set1.add(300);
        set1.add(100);
        set1.add(200);
        set1.add(500);
        set1.add(400);
        set1.add(400);
        System.out.println(set1);// [500, 400, 300, 200, 100]

        System.out.println("===========================================");
        // 按照指定规则排序---->升序
        // 创建TreeSet集合,限制集合中元素的类型为Integer类型
        TreeSet<Integer> set2 = new TreeSet<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                /*
                    指定排序规则:
                    前减后  升序
                    后减前  降序
                    前:第一个参数  后:第二个参数
                 */
                return o1 - o2;
            }
        });

        // 往集合中存储数据
        set2.add(300);
        set2.add(100);
        set2.add(200);
        set2.add(500);
        set2.add(400);
        set2.add(400);
        System.out.println(set2);// [100, 200, 300, 400, 500]
    }
}

Map集合

Map概述

image-20220709090324966

Map<K,V>集合的特点: K用来限制键的类型,V用来限制值的类型
         1.Map集合存储元素是以键值对的形式存储,每一个键值对都有键和值
         2.Map集合的键是唯一,值可以重复,如果键重复了,那么值就会被覆盖
         3.根据键取值

Map集合子类:
    - HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。
                    由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

    - LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。
               通过链表结构可以保证键值对的存取顺序一致;
               通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

    - TreeMap<K,V>:TreeMap集合和Map相比没有特有的功能,底层的数据结构是红黑树;
                可以对元素的键进行排序,排序方式有两种:自然排序和比较器排序

Map的常用方法

Map接口中定义了很多方法,常用的如下:

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
  • public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
  • public boolean containsKey(Object key):判断该集合中是否有此键
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

Map接口的方法演示



import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Test {
    public static void main(String[] args) {
        /*
             Map<K,V>的常用方法:
                - public V put(K key, V value):  把指定的键与指定的值添加到Map集合中。
                - public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
                - public V get(Object key) 根据指定的键,在Map集合中获取对应的值。

                - public boolean containsKey(Object key):判断该集合中是否有此键
                - public boolean containsValue(Object value):判断该集合中是否有此值

                - public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
                - public Collection<V> values(): 获取Map集合中所有的值,存储到Collection集合中。

                - public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。
                    Map.Entry<K,V>:表示键值对对象---把键值对包装成一个对象,该对象的类型就是Entry类型
         */
        // 创建Map集合,限制键的类型为String,值的类型为String
        Map<String, String> map = new HashMap<>();

        // 往map集合中添加键值对
        map.put("黄晓明", "杨颖");
        map.put("文章", "马伊琍");
        map.put("谢霆锋", "王菲");
        System.out.println(map);// {文章=马伊琍, 谢霆锋=王菲, 黄晓明=杨颖}

        // Map集合键唯一,如果键重复了,值会覆盖
        String v1 = map.put("文章", "姚笛");
        System.out.println("v1:"+v1);// 马伊琍
        System.out.println(map);// {文章=姚笛, 谢霆锋=王菲, 黄晓明=杨颖}

        // Map集合值可以重复
        String v2 = map.put("李亚鹏", "王菲");
        System.out.println("v2:"+v2);// null
        System.out.println(map);// {文章=姚笛, 谢霆锋=王菲, 李亚鹏=王菲, 黄晓明=杨颖}

        // 删除文章这个键对应的键值对
        String v3 = map.remove("文章");
        System.out.println("被删除键值对的值:"+v3);// 姚笛
        System.out.println(map);// {谢霆锋=王菲, 李亚鹏=王菲, 黄晓明=杨颖}

        // 获取黄晓明这个键对应的值
        String value = map.get("黄晓明");
        System.out.println("value:"+value);// 杨颖

        // 判断是否包含指定的键
        System.out.println(map.containsKey("黄晓明"));// true
        System.out.println(map.containsKey("文章"));// false

        // 判断是否包含指定的值
        System.out.println(map.containsValue("杨颖"));// true
        System.out.println(map.containsValue("马伊琍"));// false

        // 获取所有的键
        Set<String> keys = map.keySet();
        System.out.println("keys:"+keys);// [谢霆锋, 李亚鹏, 黄晓明]

        // 获取所有的值
        Collection<String> values = map.values();
        System.out.println("values:"+values);// [王菲, 王菲, 杨颖]

        // 获取Map集合中所有键值对对象
        Set<Map.Entry<String, String>> set = map.entrySet();
        System.out.println(set);// [谢霆锋=王菲, 李亚鹏=王菲, 黄晓明=杨颖]

    }
}

tips:

使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;

若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

Map的遍历

方式1:键找值方式

通过元素中的键,获取键所对应的值

分析步骤:

  1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset()
  2. 遍历键的Set集合,得到每一个键。
  3. 根据键,获取键所对应的值。方法提示:get(K key)
public class Demo {
    public static void main(String[] args) {
        // 创建Map集合对象,限制键的类型为String,值的类型为String
        Map<String, String> map = new HashMap<>();
        // 往map集合中添加键值对
        map.put("黄晓明", "杨颖");
        map.put("文章", "马伊琍");
        map.put("谢霆锋", "王菲");

        // 遍历map集合
        // 获取集合中所有的键  Set<K> keySet()方法
        Set<String> keys = map.keySet();
        // 遍历所有的键的集合
        for (String key : keys) {
            // 在循环中,根据键找值 V get(K key)方法
            String value = map.get(key);
            System.out.println("键:"+key+",值:"+value);
        }
    }
}

方式2:键值对方式

Entry<K,V>接口:简称Entry项,表示键值对对象,用来封装Map集合中的键值对
Entry<K,V>接口:是Map接口中的内部接口,在外部使用的时候是这样表示: Map.Entry<K,V>

Map集合中提供了一个方法来获取所有键值对对象:
            public Set<Map.Entry<K,V>> entrySet()

根据键值对对对象获取键和值:
            - public K getKey():获取Entry对象中的键。
            - public V getValue():获取Entry对象中的值。

Map遍历方式二:根据键值对对象的方式
            1.获取集合中所有键值对对象,以Set集合形式返回。  Set<Map.Entry<K,V>> entrySet()
            2.遍历所有键值对对象的集合,得到每一个键值对(Entry)对象。
            3.在循环中,可以使用键值对对对象获取键和值   getKey()和getValue()
public class Demo {
    public static void main(String[] args) {
        // 创建Map集合对象,限制键的类型为String,值的类型为String
        Map<String, String> map = new HashMap<>();
        // 往map集合中添加键值对
        map.put("黄晓明", "杨颖");
        map.put("文章", "马伊琍");
        map.put("谢霆锋", "王菲");

        // 获取集合中所有键值对对象  Set<Map.Entry<K,V>> entrySet()
        Set<Map.Entry<String, String>> entrySet = map.entrySet();

        // 遍历所有键值对对象的集合
        for (Map.Entry<String, String> entry : entrySet) {
            // 在循环中,可以使用键值对对对象获取键和值   getKey()和getValue()
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println("键:"+key+",值:"+value);
        }
    }
}

HashMap存储自定义类型

练习:每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

注意,学生姓名相同并且年龄相同视为同一名学生。

编写学生类:

public class Student {
    /**
     * 姓名
     */
    public String name;
    /**
     * 年龄
     */
    public int age;

    public Student() {
    }

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

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

编写测试类:

public class Demo {
    public static void main(String[] args) {
        // 创建Map集合,指定键的类型为Student,值的类型为String
        HashMap<Student,String> map = new HashMap<>();

        // 创建多个学生对象
        Student stu1 = new Student("张三", 18);
        Student stu2 = new Student("李四", 38);
        Student stu3 = new Student("王五", 28);
        Student stu4 = new Student("张三", 18);

        // 把学生对象作为键,家庭地址作为值,存储到map集合中
        map.put(stu1,"北京");
        map.put(stu2,"上海");
        map.put(stu3,"深圳");
        map.put(stu4,"广州");

        // 打印map集合
        System.out.println(map);
        System.out.println(map.size());// 3
    }
}
  • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。
  • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

LinkedHashMap介绍

我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

  • 通过链表结构可以保证元素的存取顺序一致;
  • 通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
import java.util.LinkedHashMap;

public class Test {
    public static void main(String[] args) {
        /*
            LinkedHashMap: 元素存取有序,键唯一,值可重复
                - 通过链表结构可以保证元素的存取顺序一致;
                - 通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
         */
        // 创建LinkedHashMap集合,限制键的类型为Integer,值的类型为String
        LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
        
      map.put(400, "武汉");
        map.put(400, "深圳");
        System.out.println(map);
    }
}

TreeMap集合

TreeMap介绍

TreeMap集合和Map相比没有特有的功能,底层的数据结构是红黑树;可以对元素的进行排序,排序方式有两种:自然排序比较器排序;到时使用的是哪种排序,取决于我们在创建对象的时候所使用的构造方法;

构造方法

public TreeMap()									使用自然排序
public TreeMap(Comparator<? super K> comparator) 	   通过比较器指定规则排序

案例演示

import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.TreeMap;

public class Test {
    public static void main(String[] args) {
        /*
            TreeMap集合: 键唯一,值可以重复,如果键重复了,值就覆盖,可以根据键对键值对进行排序
                public TreeMap()									根据键按照默认规则进行排序
                public TreeMap(Comparator<? super K> comparator) 	通过比较器指定规则排序
         */
        // 按照键的默认规则排序: ---->升序
        // 创建TreeMap集合,限制键的类型为Integer,值的类型为String
        TreeMap<Integer, String> map = new TreeMap<>();

        // 往map集合中添加键值对
        map.put(300, "深圳");
        map.put(100, "北京");
        map.put(200, "广州");
        map.put(500, "上海");
        map.put(400, "武汉");
        map.put(400, "深圳");
        System.out.println(map);

        System.out.println("+=================================");

        // 按照指定规则排序: ---->降序
        // 创建TreeMap集合,限制键的类型为Integer,值的类型为String
        TreeMap<Integer, String> map1 = new TreeMap<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                /*
                    前减后: 升序
                    后减前: 降序
                    前:第一个参数  后:第二个参数
                 */
                return o2 - o1;
            }
        });

        // 往map集合中添加键值对
        map1.put(300, "深圳");
        map1.put(100, "北京");
        map1.put(200, "广州");
        map1.put(500, "上海");
        map1.put(400, "武汉");
        map1.put(400, "深圳");
        System.out.println(map1);
    }
}

Map集合练习

需求

  • 输入一个字符串统计该字符串中每个字符出现次数。

分析

  • 获取一个字符串对象
  • 创建一个Map集合,键代表字符,值代表次数。
  • 遍历字符串得到每个字符。
  • 判断Map中是否有该键。
  • 如果没有,第一次出现,存储次数为1;如果有,则说明已经出现过,获取到对应的值进行++,再次存储。
  • 打印最终结果

实现

方法介绍

public boolean containKey(Object key):判断该集合中是否有此键。

代码:

import java.util.HashMap;
import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        /*
            Map集合练习:输入一个字符串,统计该字符串中每个字符出现次数。
         */
        // 0.输入一个字符串
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个字符串:");
        String s = sc.nextLine();

        // 1.创建Map集合,键的类型为Character,值的类型为Integer
        HashMap<Character, Integer> map = new HashMap<>();

        // 2.循环遍历字符串
        for (int i = 0; i < s.length(); i++) {
            // 3.在循环中获取遍历出来的字符
            char c = s.charAt(i);
            // 4.判断集合中是否存在该字符的键
            if (map.containsKey(c)) {
                // 6.如果集合中已存在该字符的键,那么就取出该字符键对应的值,然后自增1,作为新的值,重新存储到Map集合中
                Integer value = map.get(c);
                value++;
                map.put(c, value);
            } else {
                // 5.如果集合中不存在该字符的键,那么就该字符作为键,值为1,存储到Map集合中
                map.put(c, 1);
            }
        }
        // 7.循环结束,打印map集合
        System.out.println(map);
    }
}

集合的嵌套

任何集合内部都可以存储其它任何集合

List嵌套List

public class Test1 {
    public static void main(String[] args) {
        /*
            集合的嵌套:
                - List嵌套List
                - List嵌套Map
                - Map嵌套Map
            结论:任何集合内部都可以存储其它任何集合
         */
        //  List嵌套List
        // 创建一个List集合,限制元素类型为String
        List<String> list1 = new ArrayList<>();

        // 往集合中添加元素
        list1.add("王宝强");
        list1.add("贾乃亮");
        list1.add("陈羽凡");

        // 创建一个List集合,限制元素类型为String
        List<String> list2 = new ArrayList<>();

        // 往集合中添加元素
        list2.add("马蓉");
        list2.add("李小璐");
        list2.add("白百何");

        // 创建一个List集合,限制元素类型为List集合 (List集合中的元素是List集合)
        List<List<String>> list = new ArrayList<>();
        list.add(list1);
        list.add(list2);

        // 遍历
        for (List<String> e : list) {
            for (String name : e) {
                System.out.println(name);
            }
            System.out.println("=============");
        }

        System.out.println(list);
    }
}


List嵌套Map

public class Test2 {
    public static void main(String[] args) {
        /*
            List嵌套Map:

         */
        // 创建Map集合对象
        Map<String,String> map1 = new HashMap<>();
        map1.put("it001","迪丽热巴");
        map1.put("it002","古力娜扎");

        // 创建Map集合对象
        Map<String,String> map2 = new HashMap<>();
        map2.put("heima001","蔡徐坤");
        map2.put("heima002","李易峰");

        // 创建List集合,用来存储以上2个map集合
        List<Map<String,String>> list = new ArrayList<>();
        list.add(map1);
        list.add(map2);

        System.out.println(list.size()); // 2

        for (Map<String, String> map : list) {
            // 遍历获取出来的map集合对象
            Set<String> keys = map.keySet();// 获取map集合所有的键
            // 根据键找值
            for (String key : keys) {
                System.out.println(key + ","+ map.get(key));
            }
        }

    }
}

Map嵌套Map

public class Test3 {
    public static void main(String[] args) {
        /*
            Map嵌套Map:

         */
        // 创建Map集合对象
        Map<String,String> map1 = new HashMap<>();
        map1.put("it001","迪丽热巴");
        map1.put("it002","古力娜扎");

        // 创建Map集合对象
        Map<String,String> map2 = new HashMap<>();
        map2.put("heima001","蔡徐坤");
        map2.put("heima002","李易峰");

        // 创建Map集合,把以上2个Map集合作为值存储到这个map集合中
        Map<String, Map<String, String>> map = new HashMap<>();

        map.put("传智博客",map1);
        map.put("黑马程序员",map2);

        System.out.println(map.size());// 2

        // 获取map集合中的所有键
        Set<String> keys = map.keySet();
        // 遍历所有的键
        for (String key : keys) {
            // 根据键找值
            Map<String, String> value = map.get(key);
            // 遍历value这个Map集合
            Set<String> keySet = value.keySet();
            for (String k : keySet) {
                String v = value.get(k);
                System.out.println(k+","+v);
            }
        }
    }
}

模拟斗地主洗牌发牌

需求

按照斗地主的规则,完成洗牌发牌的动作。

image-20220709090840612

具体规则:

  1. 组装54张扑克牌
  2. 54张牌顺序打乱
  3. 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
  4. 查看三人各自手中的牌(按照牌的大小排序)、底牌

规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3

分析

1.准备牌:

完成数字与纸牌的映射关系:

使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。

2.洗牌:

通过数字完成洗牌发牌

3.发牌:

将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。

存放的过程中要求数字大小与斗地主规则的大小对应。

将代表不同纸牌的数字分配给不同的玩家与底牌。

4.看牌:

通过Map集合找到对应字符展示。

通过查询纸牌与数字的对应关系,由数字转成纸牌字符串再进行展示。

1587798350909

实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class Test {
    public static void main(String[] args) {
        /*
            模拟斗地主洗牌发牌:
            需求
                按照斗地主的规则,完成洗牌发牌的动作。
                具体规则:
                    1. 组装54张扑克牌
                    2. 54张牌顺序打乱
                    3. 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
                    4. 查看三人各自手中的牌(按照牌的大小排序)、底牌
                    规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3
         */
        // 造牌
        // 1.创建Map集合对象,限制键的类型为Integer,值的类型为String
        HashMap<Integer, String> pokeBox = new HashMap<>();
        // 2.创建一个List集合,表示花色集合,
        ArrayList<String> colors = new ArrayList<>();
        // 3.创建一个List集合,表示牌面值集合
        ArrayList<String> numbers = new ArrayList<>();

        // 4.往花色集合中存储4个花色
        Collections.addAll(colors, "♥", "♦", "♠", "♣");
        // 5.往牌面值集合中存储13个牌面值
        Collections.addAll(numbers, "2", "A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3");

        // 6.定义一个int类型的变量,表示牌的编号,初始值为0
        int id = 0;
        // 7.往map集合中添加大王,编号为0,添加完后编号自增1
        pokeBox.put(id++, "大王");

        // 8.往map集合中添加小王,编号为1,添加完后编号自增1
        pokeBox.put(id++, "小王");

        // 9.牌面值的集合和花色集合循环嵌套遍历,注意牌面值集合作为外层循环,花色集合作为内层循环
        for (String number : numbers) {
            for (String color : colors) {
                // 10.在循环中,遍历出来的牌面值和花色组成一张扑克牌
                String pai = color + number;
                // 11.在循环中,编号作为键,扑克牌作为值存储到map集合中,每存储一张牌后,编号自增1
                pokeBox.put(id++,pai);
            }
        }

        System.out.println(pokeBox.size());
        System.out.println(pokeBox);

        //2.洗牌 :--->洗牌的编号
        //2.1 获取所有牌的编号,返回的是所有编号的Set集合
        Set<Integer> keySet = pokeBox.keySet();

        //2.2 创建ArrayList集合,用来存储所有的牌编号
        ArrayList<Integer> idList = new ArrayList<>();

        //2.3 把keySet集合中存储的所有牌编号,存储到这个新的ArrayList集合中
        idList.addAll(keySet);

        //2.4 使用Collections.shuffle方法对新的ArrayList集合中的元素打乱顺序
        Collections.shuffle(idList);
        System.out.println("打乱顺序后的牌编号:"+idList.size());// 54
        System.out.println("打乱顺序后的牌编号:"+idList);


        // 3.发牌-->发牌的编号--->对牌的编号进行从小到大排序---->再根据排好序的编号去map集合中获取牌
        // 3.1 创建4个List集合,分别用来存储玩家一,玩家二,玩家三,底牌得到的牌编号
        ArrayList<Integer> play1Id = new ArrayList<>();
        ArrayList<Integer> play2Id = new ArrayList<>();
        ArrayList<Integer> play3Id = new ArrayList<>();
        ArrayList<Integer> diPaiId = new ArrayList<>();

        // 3.2 循环把打乱顺序的牌编号,按照规律依次发给玩家一,玩家二,玩家三,底牌
        for (int i = 0; i < idList.size(); i++) {
            // 获取牌编号
            Integer paiId = idList.get(i);
            // 三人交替摸牌
            if (i >= 51){
                diPaiId.add(paiId);
            }else if (i%3==0){
                play1Id.add(paiId);
            }else if (i%3==1){
                play2Id.add(paiId);
            }else if (i%3==2){
                play3Id.add(paiId);
            }
        }

        // 3.3 对获取到的牌编号进行从小到大排序
        Collections.sort(play1Id);
        Collections.sort(play2Id);
        Collections.sort(play3Id);
        Collections.sort(diPaiId);

        // 3.4 根据排好序的牌编号去map集合中获取牌
        // 遍历玩家一的牌编号
        System.out.print("玩家一的牌:");
        for (Integer paiId : play1Id) {// 1,2,3,4,5
            String pai = pokeBox.get(paiId);
            System.out.print(pai+" ");
        }

        System.out.println();

        // 遍历玩家二的牌编号
        System.out.print("玩家二的牌:");
        for (Integer paiId : play2Id) {
            String pai = pokeBox.get(paiId);
            System.out.print(pai+" ");
        }

        System.out.println();

        // 遍历玩家三的牌编号
        System.out.print("玩家三的牌:");
        for (Integer paiId : play3Id) {
            String pai = pokeBox.get(paiId);
            System.out.print(pai+" ");
        }

        System.out.println();

        // 遍历底牌的牌编号
        System.out.print("底牌的牌:");
        for (Integer paiId : diPaiId) {
            String pai = pokeBox.get(paiId);
            System.out.print(pai+" ");
        }
    }
}

IO流

IO概述

IO的概述

  • I : Input 输入 从其他存储设备读数据到内存中就是输入
  • O : Output 输出 从内存中写数据到其他存储设备

IO的分类

根据数据的流向分为:输入流输出流

  • 输入流 :把数据从其他设备上读取到内存中的流。
    • 字节输入流:以字节为基本单位,读数据
    • 字符输入流:以字符为基本单位,读数据
  • 输出流 :把数据从内存 中写出到其他设备上的流。
    • 字节输出流:以字节为基本单位,写出数据
    • 字符输出流:以字符为基本单位,写出数据

根据数据的类型分为:字节流字符流

  • 字节流 :以字节为单位,读写数据的流。
    • 字节输入流:以字节为基本单位,读数据
    • 字节输出流:以字节为基本单位,写出数据
  • 字符流 :以字符为单位,读写数据的流。
    • 字符输入流:以字符为基本单位,读数据
    • 字符输出流:以字符为基本单位,写出数据

IO的顶层父类

  • 字节输入流:顶层父类 InputStream 抽象类
  • 字节输出流:顶层父类 OutputStream 抽象类
  • 字符输入流:顶层父类 Reader 抽象类
  • 字符输出流:顶层父类 Writer 抽象类

注意事项

  • utf8编码一个中文占3个字节,gbk编码一个中文占2个字节
  • 如果存储和解析的编码不一致就会乱码
  • idea默认编码是utf8

字节流

字节输出流【OutputStream】

OutputStream类的概述

java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

OutputStream类的常用方法

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
  • public abstract void write(int b) :将指定的字节输出流。

小贴士:

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileOutputStream类

FileOutputStream类的概述

java.io.FileOutputStream 类是OutputStream类的子类,用来表示是文件输出流,用于将数据写出到文件。

FileOutputStream类的构造方法

  • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。

  • 构造举例,代码如下:


import java.io.File;
import java.io.FileOutputStream;

public class Test1_概述和构造方法 {
    public static void main(String[] args) throws Exception{
        /*
            FileOutputStream类:
                概述:java.io.FileOutputStream类是OutputStream类的子类,表示字节输出流,用来写出字节数据
                构造方法:
                    - public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
                    - public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。
                注意:
                    1.当你创建一个字节输出流对象时,必须传入一个文件路径。
                    2.该路径下,如果没有这个文件,会创建该文件(空的)。
                    3.如果有这个文件,会清空这个文件的数据。
         */
        // 1.创建字节输出流对象,关联day12\bbb\a.txt文件
        FileOutputStream fos1 = new FileOutputStream("day12\\bbb\\a.txt");

        // 2.创建字节输出流对象,关联day12\bbb\b.txt文件
        FileOutputStream fos2 = new FileOutputStream("day12\\bbb\\b.txt");

        // 3.创建字节输出流对象,关联day12\bbb\c.txt文件
        FileOutputStream fos3 = new FileOutputStream(new File("day12\\bbb\\c.txt"));

    }
}

FileOutputStream类的写出数据

  1. 写出字节write(int b) 方法,每次可以写出一个字节数据,代码使用演示:
public class Test2_写出单个字节数据 {
    public static void main(String[] args) throws Exception{
        /*
            FileOutputStream类的写出数据:
                - public abstract void write(int b) :将指定的字节写入输出流。
                - public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
         */
        // 创建字节输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\bbb\\a.txt");

        // 写出单个字节数据
        fos.write(97);
        fos.write(98);
        fos.write(99);

        // 关闭流,释放资源
        fos.close();
    }
}

小贴士:

  1. 虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。
  2. 流操作完毕后,必须释放系统资源,调用close方法,千万记得。
  1. 写出字节数组write(byte[] b),每次可以写出数组中的数据,代码使用演示:
public class Test3_写出字节数组数据 {
    public static void main(String[] args) throws Exception{
        /*
            FileOutputStream类的写出数据:
                - public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
                - public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
         */
        // 创建字节输出流,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\bbb\\b.txt");
        // 创建字节数组,并存储字节数据
        byte[] bys = {97,98,99,100};
        // 写出该字节数组中的字节数据
        fos.write(bys);
        // 关闭流,释放资源
        fos.close();
    }
}
  1. 写出指定长度字节数组write(byte[] b, int off, int len) ,每次写出从off索引开始,len个字节,代码使用演示:
public class Test4_写出指定范围字节数组数据 {
    public static void main(String[] args) throws Exception{
        /*
            FileOutputStream类的写出数据:
                - public void write(byte[] b, int off, int len) :写指定范围的字节数组的字节数据到输出流。
                - public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
         */
        // 创建字节输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\bbb\\c.txt");

        // 创建字节数组,存储字节数据
        byte[] bys = {97,98,99,100};

        // 写出指定范围字节数组中的字节数据
        fos.write(bys, 1,2 );

        // 关闭流,释放资源
        fos.close();
    }
}

数据追加续写

经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?

  • public FileOutputStream(File file, boolean append): 创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name, boolean append): 创建文件输出流以指定的名称写入文件。

这两个构造方法,参数中都需要传入一个boolean类型的值,true 表示追加数据,false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:

public class Test5_数据追加续写 {
    public static void main(String[] args) throws Exception{
        /*
            数据追加续写:
                - public FileOutputStream(File file, boolean append): 创建文件输出流以写入由指定的 File对象表示的文件。
                - public FileOutputStream(String name, boolean append): 创建文件输出流以指定的名称写入文件。
                参数append: 如果是true,就表示追加续写(不清空),如果是false,就清空原文件中的数据
                注意:
                    1.当你创建一个字节输出流对象时,必须传入一个文件路径。
                    2.该路径下,如果没有这个文件,会创建该文件(空的)。
                    3.如果有这个文件,并且第二个参数为true,就不清空,如果第二个参数为false,就清空
         */
        // 创建字节输出流对象,关联目的地文件路径(day12\bbb\a.txt)
        FileOutputStream fos = new FileOutputStream("day12\\bbb\\a.txt",true);
        // 写出数据
        fos.write(97);
        // 关闭流,释放资源
        fos.close();

    }
}

写出换行

Windows系统里,换行符号是\r\n 。把

以指定是否追加续写了,代码使用演示:

public class FOSWrite {
    public static void main(String[] args) throws IOException {
          // 需求: 把这首诗写入day15\\aaa\\d.txt文件中
        // String类 byte[] getBytes();把一个字符串转换为一个字节数组
        // 创建字节输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day15\\aaa\\d.txt");
        // 写出数据
        fos.write("吟诗一首".getBytes());
        fos.write("\r\n".getBytes());
        fos.write("看这风景美如画".getBytes());
        fos.write("\r\n".getBytes());
        fos.write("吟诗一首赠天下".getBytes());
        fos.write("\r\n".getBytes());
        fos.write("奈何本人没文化".getBytes());
        fos.write("\r\n".getBytes());
        fos.write("只能卧槽浪好大".getBytes());

        // 关闭流,释放资源
        fos.close();
    }
}
  • 回车符\r和换行符\n
    • 回车符:回到一行的开头(return)。
    • 换行符:下一行(newline)。
  • 系统中的换行:
    • Windows系统里,每行结尾是 回车+换行 ,即\r\n

    • Unix系统里,每行结尾只有 换行 ,即\n

    • Mac系统里,每行结尾是 回车 ,即\r。从 Mac OS X开始与Linux统一。

字节输入流【InputStream】

InputStream类的概述

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

InputStream类的常用方法

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public abstract int read(): 从输入流读取数据的下一个字节。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

小贴士:

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileInputStream类

FileInputStream类的概述

java.io.FileInputStream 类是InputStream类的子类 , 用来表示文件输入流,从文件中读取字节。

FileInputStream类的构造方法

  • FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的文件对象 file命名。
  • FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

当你创建一个输入流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException

  • 构造举例,代码如下:
public class Test1_概述和构造方法 {
    public static void main(String[] args) throws Exception{
        // 文件存在:
        // 创建字节输入流对象,关联数据源文件路径
        FileInputStream fis1 = new FileInputStream("day12\\ccc\\a.txt");
        FileInputStream fis2 = new FileInputStream(new File("day12\\ccc\\a.txt"));

        // 文件不存在:
        FileInputStream fis3 = new FileInputStream("day12\\ccc\\b.txt");// 运行报文件找不到异常FileNotFoundException

    }
}

FileInputStream类的读取数据

  1. 读取字节read方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1,代码使用演示:
public class FISRead {
    public static void main(String[] args) throws IOException{
      	// 使用文件名称创建流对象
       	FileInputStream fis = new FileInputStream("read.txt");
      	// 读取数据,返回一个字节
        int read = fis.read();
        System.out.println((char) read);
        read = fis.read();
        System.out.println((char) read);
        read = fis.read();
        System.out.println((char) read);
        read = fis.read();
        System.out.println((char) read);
        read = fis.read();
        System.out.println((char) read);
      	// 读取到末尾,返回-1
       	read = fis.read();
        System.out.println( read);
		// 关闭资源
        fis.close();
    }
}
输出结果:
a
b
c
d
e
-1

循环改进读取方式,代码使用演示:

public class FISRead {
    public static void main(String[] args) throws IOException{
      	// 使用文件名称创建流对象
       	FileInputStream fis = new FileInputStream("read.txt");
      	// 定义变量,保存数据
        int b ;
        // 循环读取
        while ((b = fis.read())!=-1) {
            System.out.println((char)b);
        }
		// 关闭资源
        fis.close();
    }
}
输出结果:
a
b
c
d
e

小贴士:

  1. 虽然读取了一个字节,但是会自动提升为int类型。
  2. 流操作完毕后,必须释放系统资源,调用close方法,千万记得。
  1. 使用字节数组读取read(byte[] b),每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1 ,代码使用演示:
public class Test3_读取字节数组长度个字节数据 {
    public static void main(String[] args) throws Exception{
        /*
             FileInputStream类读取数据:
                - public int read(byte[] b): 从输入流中读取一些字节数据,并将它们存储到字节数组 b中,返回的是读取到的字节个数。
         */
        // 创建字节输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\ccc\\b.txt");

        // 创建一个长度为2的字节数组,用来存储读取到的字节数据
        byte[] bys = new byte[2];

        // 读取字节数据
        int len1 = fis.read(bys);
        System.out.println("bys数组转换为字符串:"+new String(bys,0,len1));// ab
        System.out.println(len1);// 2

        int len2 = fis.read(bys);
        System.out.println("bys数组转换为字符串:"+new String(bys,0,len2));// cd
        System.out.println(len2);// 2

        int len3 = fis.read(bys);
        System.out.println("bys数组转换为字符串:"+new String(bys,0,len3));// e
        System.out.println(len3);// 1

        int len4 = fis.read(bys);
        //System.out.println("bys数组转换为字符串:"+new String(bys));// ed
        System.out.println(len4);// -1

      

        // 关闭流,释放资源
        fis.close();
    }
}

循环读取代码使用演示:

public class Test3_读取字节数组长度个字节数据 {
    public static void main(String[] args) throws Exception{
        /*
             FileInputStream类读取数据:
                - public int read(byte[] b): 从输入流中读取一些字节数据,并将它们存储到字节数组 b中,返回的是读取到的字节个数。
         */
        // 创建字节输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\ccc\\b.txt");

        // 创建一个长度为2的字节数组,用来存储读取到的字节数据
        byte[] bys = new byte[2];

        // 读取字节数据
        // 定义一个int类型的变量,用来存储读取到的有效字节个数
        int len;
        while ((len = fis.read(bys)) != -1){
            System.out.println(new String(bys,0,len));
        }

        // 关闭流,释放资源
        fis.close();
    }
}

小贴士:

使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。

字节流练习:图片复制

需求

  • 使用字节流拷贝一张图片

分析

1588696319583

一次读写一个字节拷贝文件思路:
                    1.创建字节输入流对象,关联数据源文件路径
                    2.创建字节输出流对象,关联目的地文件路径
                    3.定义一个变量,用来存储读取到的字节数据
                    4.循环读取
                    5.在循环中,写出数据
                    6.关闭流,释放资源
一次读写一个字节数组拷贝文件
                    1.创建字节输入流对象,关联数据源文件路径
                    2.创建字节输出流对象,关联目的地文件路径
                    3.定义一个字节数组,用来存储读取到的字节数据
                    3.定义一个变量,用来存储读取到的字节个数
                    4.循环读取
                    5.在循环中,写出数据
                    6.关闭流,释放资源

实现

复制图片文件,代码使用演示:


public class Test {
    public static void main(String[] args) throws Exception{
        /*
            练习:字节流练习:图片复制
            一次读写一个字节拷贝文件:
                1.创建字节输入流对象,关联数据源文件路径
                2.创建字节输出流对象,关联目的地文件路径
                3.定义一个变量用来存储读取到的字节数据
                4.循环读取
                5.在循环中,写出数据
                6.关闭流,释放资源

            一次读写一个字节数组拷贝文件:
                1.创建字节输入流对象,关联数据源文件路径
                2.创建字节输出流对象,关联目的地文件路径
                3.定义一个字节数组,用来存储读取到的字节数据
                3.定义一个变量用来存储读取到的字节个数
                4.循环读取
                5.在循环中,写出数据
                6.关闭流,释放资源
         */
        //一次读写一个字节拷贝文件:
       /* // 1.创建字节输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\aaa\\hb.jpg");
        // 2.创建字节输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\ccc\\hbCopy1.jpg");
        // 3.定义一个变量用来存储读取到的字节数据
        int len;
        // 4.循环读取
        while ((len = fis.read()) != -1) {
            // 5.在循环中,写出数据
            fos.write(len);
        }
        // 6.关闭流,释放资源
        fos.close();
        fis.close();*/

        //  一次读写一个字节数组拷贝文件:
        // 1.创建字节输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\aaa\\hb.jpg");
        // 2.创建字节输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\ccc\\hbCopy2.jpg");
        // 3.定义一个字节数组,用来存储读取到的字节数据
        byte[] bys = new byte[8192];
        // 3.定义一个int变量,用来存储读取到的字节个数
        int len;
        // 4.循环读取
        while ((len = fis.read(bys)) != -1) {
            // 5.在循环中,写出数据
            fos.write(bys,0,len);
        }
        // 6.关闭流,释放资源
        fos.close();
        fis.close();
    }
}

小贴士:

流的关闭原则:先开后关,后开先关。

字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。


public class Test {
    public static void main(String[] args) throws Exception{
        // 需求:使用字节流读取出a.txt文件中的每个字符数据并打印到控制台
        // 1.创建字节输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\ddd\\a.txt");

        // 2.定义一个int变量,用来存储读取到的字节数据
        int len;

        // 3.循环读取
        while ((len = fis.read()) != -1){
            System.out.println((char)len);
        }

        // 4.释放资源
        fis.close();
    }
}

字符输入流【Reader】

字符输入流Reader类的概述

java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

字符输入流Reader类的常用方法

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流读取一个字符。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。

FileReader类

FileReader类的概述

java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

FileReader类的构造方法

  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。

  • 构造举例,代码如下:
public class Test1_概述和构造方法 {
    public static void main(String[] args) throws Exception{
        /*
            概述:java.io.FileReader类继承Reader类,表示字符输入流,用来读取字符数据
            构造方法:
                - FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
                - FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。
            注意:
                    1.当你创建一个输入流对象时,必须传入一个文件路径。
                    2.该路径下,如果没有该文件,会抛出FileNotFoundException
         */
        // 文件存在:
        // 创建字符输入流对象,关联数据源文件路径
        FileReader fr1 = new FileReader("day12\\ddd\\a.txt");
        FileReader fr2 = new FileReader(new File("day12\\ddd\\a.txt"));

        // 文件不存在:
        FileReader fr3 = new FileReader("day12\\ddd\\b.txt");// 报文件找不到异常
    }
}

FileReader类读取数据

  1. 读取字符read方法,每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1,循环读取,代码使用演示:
public class FRRead {
    public static void main(String[] args) throws IOException {
      	// 使用文件名称创建流对象
       	FileReader fr = new FileReader("read.txt");
      	// 定义变量,保存数据
        int b ;
        // 循环读取
        while ((b = fr.read())!=-1) {
            System.out.println((char)b);
        }
		// 关闭资源
        fr.close();
    }
}
输出结果:
黑
马
程
序
员

小贴士:虽然读取了一个字符,但是会自动提升为int类型。

  1. 使用字符数组读取read(char[] cbuf),每次读取多个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回-1 ,代码使用演示:
public class FRRead {
    public static void main(String[] args) throws IOException {
      	// 使用文件名称创建流对象
       	FileReader fr = new FileReader("read.txt");
      	// 定义变量,保存有效字符个数
        int len ;
        // 定义字符数组,作为装字符数据的容器
         char[] cbuf = new char[2];
        // 循环读取
        while ((len = fr.read(cbuf))!=-1) {
            System.out.println(new String(cbuf));
        }
		// 关闭资源
        fr.close();
    }
}
输出结果:
黑马
程序
员序

获取有效的字符改进,代码使用演示:

public class FISRead {
    public static void main(String[] args) throws IOException {
      	// 使用文件名称创建流对象
       	FileReader fr = new FileReader("read.txt");
      	// 定义变量,保存有效字符个数
        int len ;
        // 定义字符数组,作为装字符数据的容器
        char[] cbuf = new char[2];
        // 循环读取
        while ((len = fr.read(cbuf))!=-1) {
            System.out.println(new String(cbuf,0,len));
        }
    	// 关闭资源
        fr.close();
    }
}

输出结果:
黑马
程序
员

字符输出流【Writer】

字符输出流Writer类的概述

java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

字符输出流Writer类的常用方法

  • public abstract void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public abstract void flush() :刷新此输出流并强制任何缓冲的输出字符被写出。
  • public void write(int c) :写出一个字符。
  • public void write(char[] cbuf):将 b.length字符从指定的字符数组写出此输出流。
  • public abstract void write(char[] b, int off, int len) :从指定的字符数组写出 len字符,从偏移量 off开始输出到此输出流。
  • public void write(String str) :写出一个字符串。
  • public void write(String str,int off,int len) :写出一个字符串的一部分。

FileWriter类

FileWriter类的概述

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

FileWriter类的构造方法

  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。
  • FileWriter(File file,boolean append): 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName,boolean append): 创建一个新的 FileWriter,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。

  • 构造举例,代码如下:
public class Test1_概述和构造方法 {
    public static void main(String[] args) throws Exception{
        /*
            概述:java.io.FileWriter是Writer的子类,表示字符输出流,可以用来写出字符数据
            构造方法:
                - FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
                - FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。
                - FileWriter(File file,boolean append): 创建一个新的 FileWriter,给定要读取的File对象。
                - FileWriter(String fileName,boolean append): 创建一个新的 FileWriter,给定要读取的文件的名称。
            注意:
                    1.当你创建一个字符输出流对象时,必须传入一个文件路径。
                    2.前面2个构造方法,传入的路径,如果没有这个文件,会创建该文件(空的),如果有这个文件,就会清空。
                    3.后面2个构造方法,传入的路径, 如果没有这个文件,会创建该文件(空的),如果有这个文件,并且第二个参数为true,就不清空,如果第二个参数为false,就清空
         */
        // 文件存在
        FileWriter fw1 = new FileWriter("day12\\ddd\\b.txt",true);

        // 文件不存在
        FileWriter fw2 = new FileWriter("day12\\ddd\\c.txt");

    }
}

FileWriter类写出数据

写出字符write(int b) 方法,每次可以写出一个字符数据,代码使用演示:

public class Test2_写单个字符 {
    public static void main(String[] args) throws Exception{
        /*
            - public void write(int c) :写出一个字符。
         */
        // 创建字符输出流对象,关联目的地文件路径
        FileWriter fw = new FileWriter("day12\\ddd\\d.txt");

        // 写出单个字符
        fw.write('a');
        fw.write('b');

        // 关闭流,释放资源
        fw.close();
    }
}

小贴士:

  1. 虽然参数为int类型四个字节,但是只会保留一个字符的信息写出。
  2. 未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。
  1. 写出字符数组write(char[] cbuf)write(char[] cbuf, int off, int len) ,每次可以写出字符数组中的数据,用法类似FileOutputStream,代码使用演示:
public class FWWrite {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("fw.txt");     
      	// 字符串转换为字节数组
      	char[] chars = "白马程序员".toCharArray();
      
      	// 写出字符数组
      	fw.write(chars); // 白马程序员
        
		// 写出从索引2开始,2个字节。索引2是'程',两个字节,也就是'程序'。
        fw.write(b,2,2); // 程序
      
      	// 关闭资源
        fos.close();
    }
}
  1. 写出字符串write(String str)write(String str, int off, int len) ,每次可以写出字符串中的数据,更为方便,代码使用演示:
public class FWWrite {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("fw.txt");     
      	// 字符串
      	String msg = "白马程序员";
      
      	// 写出字符数组
      	fw.write(msg); //白马程序员
      
		// 写出从索引2开始,2个字节。索引2是'程',两个字节,也就是'程序'。
        fw.write(msg,2,2);	// 程序
      	
        // 关闭资源
        fos.close();
    }
}
  1. 续写和换行:操作类似于FileOutputStream。
public class FWWrite {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象,可以续写数据
        FileWriter fw = new FileWriter("fw.txt",true);     
      	// 写出字符串
        fw.write("白马");
      	// 写出换行
      	fw.write("\r\n");
      	// 写出字符串
  		fw.write("程序员");
      	// 关闭资源
        fw.close();
    }
}
输出结果:
黑马
程序员

小贴士:字符流,只能操作文本文件,不能操作图片,视频等非文本文件。

当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流

关闭和刷新

因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法了。

  • flush :刷新缓冲区,流对象可以继续使用。
  • close :关闭流,释放系统资源。关闭前会刷新缓冲区。

代码使用演示:

public class FWWrite {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("fw.txt");
        // 写出数据,通过flush
        fw.write('刷'); // 写出第1个字符
        fw.flush();
        fw.write('新'); // 继续写出第2个字符,写出成功
        fw.flush();
      
      	// 写出数据,通过close
        fw.write('关'); // 写出第1个字符
        fw.close();
        fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
        fw.close();
    }
}

小贴士:即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。

IO资源的处理

JDK7前处理

之前的入门练习,我们一直把异常抛出,而实际开发中并不能这样处理,建议使用try...catch...finally 代码块,处理异常部分,代码使用演示:


public class Test {
    public static void main(String[] args)  {
        /*
            JDK7前处理: try...catch...finally
         */
        // 一次读写一个字节数组拷贝文件
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try{
            // 1.创建字节输入流对象,关联数据源文件路径
            fis = new FileInputStream("day13\\aaa\\hbCopy1.jpg");

            // 2.创建字节输出流对象,关联目的地文件路径
            fos = new FileOutputStream("day13\\aaa\\hbCopy3.jpg");

            // 3.定义一个字节数组,用来存储读取到的字节数据
            byte[] bys = new byte[8192];

            // 3.定义一个int变量,用来存储读取到的字节个数
            int len;

            // 4.循环读取数据
            while ((len = fis.read(bys)) != -1) {
                // 5.在循环中,写出数据
                fos.write(bys ,0, len);
            }
          
        }catch (Exception e){
            System.out.println("出现了异常");
        }finally {
            // 一般正常情况永远都会执行,所以一般用来释放资源
            // 6.关闭流,释放资源
            try {
                if (fos != null){
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    if (fis != null){
                        fis.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

JDK7的处理

还可以使用JDK7优化后的try-with-resource 语句,该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。

格式:

try (创建流对象语句,如果多个,使用';'隔开) {
	// 读写数据
} catch (IOException e) {
	e.printStackTrace();
}

代码使用演示:


public class Test {
    public static void main(String[] args) {
        /*
            jdk7的处理:try-with-resource 语句
            特点:try-with-resource 语句,该语句确保了每个资源在语句结束时关闭。
            格式:
                try (创建流对象语句,如果多个,使用';'隔开) {
                    // 读写数据
                } catch (IOException e) {
                    e.printStackTrace();
                }
         */
        // 一次读写一个字节数组拷贝文件
        try (
                // 1.创建字节输入流对象,关联数据源文件路径
                FileInputStream fis = new FileInputStream("day13\\aaa\\hbCopy1.jpg");

                // 2.创建字节输出流对象,关联目的地文件路径
                FileOutputStream fos = new FileOutputStream("day13\\aaa\\hbCopy4.jpg");
        ) {


            // 3.定义一个字节数组,用来存储读取到的字节数据
            byte[] bys = new byte[8192];

            // 3.定义一个int变量,用来存储读取到的字节个数
            int len;

            // 4.循环读取数据
            while ((len = fis.read(bys)) != -1) {
                // 5.在循环中,写出数据
                fos.write(bys, 0, len);
            }

        } catch (Exception e) {
            System.out.println("出现了异常");
        }


    }
}

属性集

Properties类

Properties类的概述

java.util.Properties 继承于 Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。

Properties类的构造方法

  • public Properties() :创建一个空的属性列表。

Properties类存储方法

  • public Object setProperty(String key, String value) : 保存一对属性。
  • public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。
  • public Set<String> stringPropertyNames() :所有键的名称的集合。

public class Test {
    public static void main(String[] args) {
        /*
            Properties类的使用:
                概述:java.util.Properties 继承于Hashtable ,来表示一个持久的属性集
                特点:
                    1.Properties当成Map集合使用,键和值的类型为Object类型
                    2.Properties当成属性集使用,键和值的类型为String类型
                构造方法:
                    public Properties() :创建一个空的属性列表。
                成员方法:
                    - public Object setProperty(String key, String value) : 保存一对属性。
                    - public Set<String> stringPropertyNames() :所有键的名称的集合。
                    - public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。
         */
        // 创建Properties对象
        Properties pro = new Properties();

        // 存储键值对
        pro.setProperty("k1", "v1");
        pro.setProperty("k2", "v2");
        pro.setProperty("k3", "v3");
        pro.setProperty("k4", "v4");
        System.out.println(pro);

        // 获取所有的键
        Set<String> keys = pro.stringPropertyNames();
        System.out.println(keys);

        // 根据键找值
        for (String key : keys) {
            String value = pro.getProperty(key);
            System.out.println(key+","+value);
        }
    }
}

Properties类与流相关的方法

  • public void load(InputStream inStream): 从字节输入流中读取键值对。

参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:

filename=a.txt
length=209385038
location=D:\a.txt

加载代码演示:

public class ProDemo2 {
    public static void main(String[] args) throws FileNotFoundException {
        // 创建属性集对象
        Properties pro = new Properties();
        // 加载文本中信息到属性集
        pro.load(new FileInputStream("read.txt"));
        // 遍历集合并打印
        Set<String> strings = pro.stringPropertyNames();
        for (String key : strings ) {
          	System.out.println(key+" -- "+pro.getProperty(key));
        }
     }
}
输出结果:
filename -- a.txt
length -- 209385038
location -- D:\a.txt

小贴士:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

Properties开发中的使用


public class Test {
    public static void main(String[] args) throws Exception {
        /*
            扩展--Properties开发中的使用:
                1.开发中的配置文件一般是后缀为.properties的文件
                2.开发中的配置文件一般放在src目录下
                3.开发中,配置文件中的内容一般不出现中文
                4.开发中,一般只会去配置文件中读取数据


                public void store(OutputStream out, String comments):把Properties对象中的键值对写回配置文件中
                public void store(Writer w, String comments):把Properties对象中的键值对写回配置文件中
                public void load(Reader reader)
         */
        // 创建Properties对象
        Properties pro = new Properties();

        // 调用load方法加载配置文件
        //pro.load(new FileInputStream("day13\\src\\db.properties"));

        // 了解:直接获取一个流,该流的默认路径就是已经到了src
        InputStream is = Test.class.getClassLoader().getResourceAsStream("db.properties");
        pro.load(is);

        // 获取数据,打印
        // 获取pro对象的所有键
        Set<String> keys = pro.stringPropertyNames();
        // 循环遍历所有的键
        for (String key : keys) {
            // 根据键找值
            String value = pro.getProperty(key);
            System.out.println(key+","+value);
        }

        System.out.println("===============扩展:添加一个键值对到配置文件中===============");
        // 往pro对象中添加一个键值对: k=v
        pro.setProperty("k", "v");

        // 把pro对象中所有的键值对重新写回db.properties文件中
        pro.store(new FileOutputStream("day13\\src\\db.properties"), "itheima");

        System.out.println("===============扩展:修改配置文件中的键值对数据===============");
        pro.setProperty("password", "654321");
        pro.store(new FileOutputStream("day13\\src\\db.properties"), "itcast");


    }
}


配置文件:
#itcast
#Tue Sep 22 10:24:20 CST 2020
password=654321
k=v
class=java.lang.String
url=http\://www.baidu.com
username=admin

缓冲流

缓冲流

昨天学习了基本的一些流,作为IO流的入门,今天我们要见识一些更强大的流。比如能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。

缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流BufferedInputStreamBufferedOutputStream
  • 字符缓冲流BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

字节缓冲流

字节缓冲流的构造方法

  • public BufferedInputStream(InputStream in) :创建一个 新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。

构造举例,代码如下:

// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

拷贝文件效率测试

查询API,缓冲流读写方法与基本的流是一致的,我们通过复制大文件(375MB),测试它的效率。

  1. 基本流,代码如下:
 // 普通流拷贝jdk9.exe文件
    private static void method02() throws IOException {
        // 0.获取当前系统时间距离标准基准时间的毫秒值
        long start = System.currentTimeMillis();

        // 1.创建输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day13\\aaa\\jdk9.exe");

        // 2.创建输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day13\\aaa\\jdk9Copy1.exe");

        // 3.定义一个int变量,用来存储读取到的字节数据
        int len = 0;

        // 4.循环读取数据
        while ((len = fis.read()) != -1) {
            // 5.在循环中,写出数据
            fos.write(len);
        }
        // 6.关闭流,释放资源
        fos.close();
        fis.close();

        // 7.获取当前系统时间距离标准基准时间的毫秒值
        long end = System.currentTimeMillis();

        // 8.计算2个时间之差
        System.out.println("总共花了:" + (end - start) + "毫秒");// 至少大概十多分钟...
    }

  1. 缓冲流,代码如下:
private static void method03() throws IOException {
        // 字节缓冲流拷贝jdk9.exe文件
        // 0.获取当前系统时间距离标准基准时间的毫秒值
        long start = System.currentTimeMillis();

        // 1.创建输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day13\\aaa\\jdk9.exe");
        BufferedInputStream bis = new BufferedInputStream(fis);

        // 2.创建输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day13\\aaa\\jdk9Copy2.exe");
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        // 3.定义一个int变量,用来存储读取到的字节数据
        int len;

        // 4.循环读取数据
        while ((len = bis.read()) != -1) {
            // 5.在循环中,写出数据
            bos.write(len);
        }

        // 6.关闭流,释放资源
        bos.close();
        bis.close();

        // 7.获取当前系统时间距离标准基准时间的毫秒值
        long end = System.currentTimeMillis();

        // 8.计算2个时间之差
        System.out.println("总共花了:" + (end - start) + "毫秒");// 大概33秒
    }

如何更快呢?

使用数组的方式,代码如下:

   public static void main(String[] args) throws Exception {
        /*
            字节缓冲流的使用:
                特点:读写效率高
                构造方法:
                    - public BufferedInputStream(InputStream in) :创建一个 新的缓冲输入流。
                    - public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。
         */
        // 拷贝文件效率测试
        // 0.获取当前系统时间距离标准基准时间的毫秒值
        long start = System.currentTimeMillis();

        // 1.创建输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day13\\aaa\\jdk9.exe");
        BufferedInputStream bis = new BufferedInputStream(fis);

        // 2.创建输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day13\\aaa\\jdk9Copy3.exe");
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        // 3.定义一个字节数组,用来存储读取到的字节数据
        byte[] bys = new byte[8192];

        // 3.定义一个int变量,用来存储读取到的字节个数
        int len;

        // 4.循环读取数据
        while ((len = bis.read(bys)) != -1) {
            // 5.在循环中,写出数据
            bos.write(bys,0,len);
        }

        // 6.关闭流,释放资源
        bos.close();
        bis.close();

        // 7.获取当前系统时间距离标准基准时间的毫秒值
        long end = System.currentTimeMillis();

        // 8.计算2个时间之差
        System.out.println("总共花了:" + (end - start) + "毫秒");// 大概3秒

    }

字符缓冲流

字符缓冲流的构造方法

  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

构造举例,代码如下:

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

字符缓冲流的特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。

readLine方法演示,代码如下:

 private static void method01() throws IOException {
        // 字符缓冲输入流读文本文件(一行一行读)
        // 创建字符缓冲输入流对象,关联数据源文件路径
        FileReader fr = new FileReader("day13\\aaa\\b.txt");
        BufferedReader br = new BufferedReader(fr);

        // 定义一个String变量,用来存储读取到的行数据
        String line;

        // 循环读取行数据
        while ((line = br.readLine()) != null){
            // 打印数据
            System.out.println(line);
        }

        // 关闭流,释放资源
        br.close();
    }

newLine方法演示,代码如下:

public static void main(String[] args) throws Exception{
      /*
          字符缓冲流:
              构造方法:
                  - public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
                  - public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

              特有方法:
                  - BufferedReader:public String readLine(): 读一行文字。如果已到达流末尾,则返回 null
                  - BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。
       */
      // 字符缓冲输出流,写数据到文本中
      // 创建字符缓冲输出流对象,关联目的地文件路径
      FileWriter fw = new FileWriter("day13\\aaa\\c.txt");
      BufferedWriter bw = new BufferedWriter(fw);

      // 写出数据
      bw.write("静夜思");
      bw.newLine();

      bw.write("床前明月光");
      bw.newLine();

      bw.write("疑是地上霜");
      bw.newLine();

      bw.write("举头望明月");
      bw.newLine();

      bw.write("低头思故乡");

      // 关闭流,释放资源
      bw.close();

  }

文本排序

需求

请将文本信息恢复顺序。

3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。

分析

  1. 逐行读取文本信息。
  2. 解析文本信息到集合中。
  3. 遍历集合,按顺序,写出文本信息。

实现


public class Test {
    public static void main(String[] args) throws Exception {
        // 需求:请将文本信息恢复顺序。
        // 分析:
        // 1.创建ArrayList集合,用来存储读取到的行数据
        ArrayList<String> list = new ArrayList<>();

        // 2.创建字符缓冲输入流对象,关联数据源文件路径
        FileReader fr = new FileReader("day13\\aaa\\d.txt");
        BufferedReader br = new BufferedReader(fr);

        // 3.定义一个字符串变量,用来存储读取到的字符串数据
        String line;

        // 4.循环读取数据
        while ((line = br.readLine()) != null) {
            // 5.在循环中,把读取到的行数据存储到集合中
            list.add(line);
        }
        // 6.关闭流,释放资源
        br.close();

        // 7.对集合中的元素进行排序
        Collections.sort(list);

        // 8.创建字符缓冲输出流对象,关联目的地文件路径
        FileWriter fw = new FileWriter("day13\\aaa\\d.txt");
        BufferedWriter bw = new BufferedWriter(fw);

        // 9.循环遍历集合
        for (String s : list) {
            // 10.在循环中,把遍历出来的元素,写回文件中
            bw.write(s);
            bw.newLine();
        }
        // 11.关闭流,释放资源
        bw.close();
    }
}

转换流

字符编码和字符集

字符编码的概述

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

  • 字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。

字符集的概述

  • 字符集 Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。

可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

  • ASCII字符集
    • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
    • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符
  • ISO-8859-1字符集
    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
    • ISO-5559-1使用单字节编码,兼容ASCII编码。
  • GBxxx字符集
    • GB就是国标的意思,是为了显示中文而设计的一套字符集。
    • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
    • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
    • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
  • Unicode字符集
    • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。
    • 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
    • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
      1. 128个US-ASCII字符,只需一个字节编码。
      2. 拉丁文等字符,需要二个字节编码。
      3. 大部分常用字(含中文),使用三个字节编码。
      4. 其他极少使用的Unicode辅助字符,使用四字节编码。

编码引出的问题

在IDEA中,使用FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。

public class ReaderDemo {
    public static void main(String[] args) throws IOException {
        FileReader fileReader = new FileReader("E:\\File_GBK.txt");
        int read;
        while ((read = fileReader.read()) != -1) {
            System.out.print((char)read);
        }
        fileReader.close();
    }
}
输出结果:
���

那么如何读取GBK编码的文件呢?

InputStreamReader类

InputStreamReader类的概述

转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

InputStreamReader类的构造方法

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

InputStreamReader类指定编码读取

public class ReaderDemo2 {
    public static void main(String[] args) throws IOException {
      	// 定义文件路径,文件为gbk编码
        String FileName = "E:\\file_gbk.txt";
      	// 创建流对象,默认UTF8编码
        InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName));
      	// 创建流对象,指定GBK编码
        InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK");
        
        
		// 定义变量,保存字符
        int read;
      	// 使用默认编码字符流读取,乱码
        while ((read = isr.read()) != -1) {
            System.out.print((char)read); // ��Һ�
        }
        isr.close();
      
      	// 使用指定编码字符流读取,正常解析
        while ((read = isr2.read()) != -1) {
            System.out.print((char)read);// 大家好
        }
        isr2.close();
    }
}

OutputStreamWriter类

OutputStreamWriter类的概述

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

OutputStreamWriter类的构造方法

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。 idea默认的是utf8
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

构造举例,代码如下:

OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");

OutputStreamWriter类指定编码读取

public class OutputDemo {
    public static void main(String[] args) throws IOException {
      	// 定义文件路径
        String FileName = "E:\\out.txt";
      	// 创建流对象,默认UTF8编码
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
        // 写出数据
      	osw.write("你好"); // 保存为6个字节
        osw.close();
      	
		// 定义文件路径
		String FileName2 = "E:\\out2.txt";
     	// 创建流对象,指定GBK编码
        OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK");
        // 写出数据
      	osw2.write("你好");// 保存为4个字节
        osw2.close();
    }
}

转换流理解图解

转换流是字节与字符间的桥梁!

转换文件编码

需求

  • 将GBK编码的文本文件,转换为UTF-8编码的文本文件。

分析

  1. 指定GBK编码的转换流,读取文本文件。
  2. 使用UTF-8编码的转换流,写出文本文件。

实现


public class Test {
    public static void main(String[] args) throws Exception{
        // 需求: 将GBK编码的文本文件,转换为UTF-8编码的文本文件。
        // 1.创建转换输入流对象,指定gbk编码,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day13\\bbb\\gbk.txt");
        InputStreamReader isr = new InputStreamReader(fis,"gbk");

        // 2.创建转换输出流对象,指定utf8编码,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day13\\bbb\\gbk_utf8.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos,"utf8");

        // 3.定义一个int变量,用来存储读取到的字符数据
        int len;

        // 4.循环读取
        while ((len = isr.read()) != -1) {
            // 5.写出数据
            osw.write(len);
        }
        // 6.释放资源
        osw.close();
        isr.close();

    }
}

序列化

序列化和反序列化的概念

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:

ObjectOutputStream类

ObjectOutputStream类的概述

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

ObjectOutputStream类构造方法

  • public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStream的ObjectOutputStream。

构造举例,代码如下:

FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

ObjectOutputStream类序列化操作

  1. 一个对象要想序列化,必须满足两个条件:
  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口
  • 该类的所有属性必须是可序列化的。

public class Student implements Serializable {
    public String name;// 姓名
    public int age;

    Animal anl;

    public Student() {
    }

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

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


public class Animal implements Serializable {
}

2.写出对象方法

  • public final void writeObject (Object obj) : 将指定的对象写出。
public class Test {
    public static void main(String[] args) throws Exception{
        /*
            ObjectOutputStream类:
                概述:java.io.ObjectOutputStream 类,是OutputStream类的子类.
                特点: 可以将java对象以字节的形式存储到文件中,实现对象的持久保存
                构造方法:
                    public ObjectOutputStream(OutputStream out): 创建一个指定OutputStream的ObjectOutputStream。
                成员方法:
                    public final void writeObject (Object obj) : 将指定的对象写出。

                要求: 需要序列化的对象所属的类必须实现序列化接口Serializable
         */
        // 需求: 把Student对象写出到指定文件中
        // 1.创建Student对象
        Student stu = new Student("张三",18);
		 stu.anl = new Animal();
        
        // 2.创建序列化流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day13\\ccc\\a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);

        // 3.写出对象
        oos.writeObject(stu);

        // 4.关闭流,释放资源
        oos.close();
    }
}

ObjectInputStream类

ObjectInputStream类的概述

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

ObjectInputStream类构造方法

  • public ObjectInputStream(InputStream in) : 创建一个指定InputStream的ObjectInputStream。

ObjectInputStream类反序列化操作1

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

  • public final Object readObject () : 读取一个对象。

public class Test {
    public static void main(String[] args) throws Exception{
        /*
            ObjectInputStream类的使用:
                概述:java.io.ObjectInputStream 类,是InputStream类的子类.
                特点: 可以将文件中对象的字节数据重构成一个对象
                构造方法:
                    public ObjectInputStream(InputStream in): 创建一个指定InputStream的ObjectInputStream。
                成员方法:
                    public final Object readObject () : 重构对象
         */
        // 需求:把a.txt文件中的Student对象,重构出来
        // 1.创建反序列化流对象,关联目的地文件路径
        FileInputStream fis = new FileInputStream("day13\\ccc\\a.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);

        // 2.读取\重构对象
        Student stu = (Student) ois.readObject();
        System.out.println(stu);

        // 3.关闭流,释放资源
        ois.close();
    }
}

序列化和反序列化注意事项

序列化的注意事项

  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。

    
    public class Student implements Serializable {
        public String name;// 姓名
        // 不要序列化,使用transient关键字修饰
        public transient int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    

反序列化的注意事项

  • 对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

  • 另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

    • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
    • 该类包含未知数据类型
    • 该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

public class Employee implements java.io.Serializable {
     // 加入序列版本号
     private static final long serialVersionUID = 1L;
     public String name;
     public String address;
     // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
     public int eid; 

     public void addressCheck() {
         System.out.println("Address  check : " + name + " -- " + address);
     }
}

序列化集合

需求

  1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
  2. 反序列化list.txt ,并遍历集合,打印对象信息。

分析

  1. 把若干学生对象 ,保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的学生信息

实现

public class SerTest {
	public static void main(String[] args) throws Exception {
		// 创建 学生对象
		Student student = new Student("老王", "laow");
		Student student2 = new Student("老张", "laoz");
		Student student3 = new Student("老李", "laol");

		ArrayList<Student> arrayList = new ArrayList<>();
		arrayList.add(student);
		arrayList.add(student2);
		arrayList.add(student3);
		// 序列化操作
		// serializ(arrayList);
		
		// 反序列化  
		ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("list.txt"));
		// 读取对象,强转为ArrayList类型
		ArrayList<Student> list  = (ArrayList<Student>)ois.readObject();
		
      	for (int i = 0; i < list.size(); i++ ){
          	Student s = list.get(i);
        	System.out.println(s.getName()+"--"+ s.getPwd());
      	}
	}

	private static void serializ(ArrayList<Student> arrayList) throws Exception {
		// 创建 序列化流 
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
		// 写出对象
		oos.writeObject(arrayList);
		// 释放资源
		oos.close();
	}
}

打印流

打印流的概述

平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

打印流的使用

  • public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

构造举例,代码如下:

PrintStream ps = new PrintStream("ps.txt");

System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",将数据输出到指定文本文件中。


public class Test {
    public static void main(String[] args) throws Exception{
        /*
            打印流的使用:
               概述:java.io.PrintStream类,是一个字节输出流
               特点: 该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
               构造方法:
                    public PrintStream(String fileName): 使用指定的文件名创建一个新的打印流。
               成员方法:
                    void print(任意类型);  不换行打印数据
                    void println(任意类型);换行打印数据
         */
        // 需求:打印各种数据到day13\\ccc\\d.txt文件中
        // 1.创建打印流对象,关联目的地文件路径
        PrintStream ps = new PrintStream("day13\\ccc\\d.txt");

        // 2.打印数据
        // 换行
        ps.println(97);// 打印整数
        ps.println('a');// 打印字符
        ps.println(3.14);// 打印小数
        ps.println(true);// 打印boolean类型
        ps.println("itheima");// 打印字符串
        // 不换行
        ps.print(97);// 打印整数
        ps.print('a');// 打印字符
        ps.print(3.14);// 打印小数
        ps.print(true);// 打印boolean类型
        ps.print("itheima");// 打印字符串

        // 3.关闭流,释放资源
        ps.close();

        System.out.println("===================扩展====================");
        System.out.println(97);// 打印到控制台
        System.out.println('a');// 打印到控制台
        // 说明系统的打印流对象,关联的路径就是控制台
        // 需求:改变系统打印流对象,关联的路径为day13\ccc\e.txt
        PrintStream ps1 = new PrintStream("day13\\ccc\\e.txt");
        System.setOut(ps1);
        System.out.println(97);// 打印到e.txt
        System.out.println('a');// 打印到e.txt

    }
}

装饰设计模式

装饰模式概述

在我们今天所学的缓冲流中涉及到java的一种设计模式,叫做装饰模式,我们来认识并学习一下这个设计模式。

装饰模式指的是在不改变原类, 不使用继承的基础上,动态地扩展一个对象的功能。

装饰模式遵循原则:

  1. 装饰类和被装饰类必须实现相同的接口
  2. 在装饰类中必须传入被装饰类的引用
  3. 在装饰类中对需要扩展的方法进行扩展
  4. 在装饰类中对不需要扩展的方法调用被装饰类中的同名方法

案例演示

准备环境

  1. 编写一个Star接口, 提供sing 和 dance抽象方法
  2. 编写一个LiuDeHua类,实现Star接口,重写抽象方法
public interface Star {
    public void sing();
    public void dance();
}
public class LiuDeHua implements Star {
    @Override
    public void sing() {
        System.out.println("刘德华在唱忘情水...");
    }
    @Override
    public void dance() {
        System.out.println("刘德华在跳街舞...");
    }
}

需求

​ 在不改变原类的基础上对LiuDeHua类的sing方法进行扩展

实现步骤

  1. 编写一个LiuDeHuaWarpper类, 实现Star接口,重写抽象方法
  2. 提供LiuDeHuaWarpper类的有参构造, 传入LiuDeHua类对象
  3. 在LiuDeHuaWarpper类中对需要增强的sing方法进行增强
  4. 在LiuDeHuaWarpper类对不需要增强的方法调用LiuDeHua类中的同名方法

实现代码如下

LiuDeHua类: 被装饰类

LiuDeHuaWarpper类: 我们称之为装饰类

/*
	装饰模式遵循原则:
		装饰类和被装饰类必须实现相同的接口
		在装饰类中必须传入被装饰类的引用
		在装饰类中对需要扩展的方法进行扩展
		在装饰类中对不需要扩展的方法调用被装饰类中的同名方法
*/
public class LiuDeHuaWarpper implements Star {
    // 存放被装饰类的引用
    private LiuDeHua liuDeHua;
    // 通过构造器传入被装饰类对象
    public LiuDeHuaWarpper(LiuDeHua liuDeHua){
        this.liuDeHua = liuDeHua;
    }
    @Override
    public void sing() {
        // 对需要扩展的方法进行扩展增强
        System.out.println("刘德华在鸟巢的舞台上演唱忘情水.");
    }
    @Override
    public void dance() {
        // 不需要增强的方法调用被装饰类中的同名方法
        liuDeHua.dance();
    }
}

测试结果

public static void main(String[] args) {
    // 创建被装饰类对象
    LiuDeHua liuDeHua = new LiuDeHua();
    // 创建装饰类对象,被传入被装饰类
    LiuDeHuaWarpper liuDeHuaWarpper = new LiuDeHuaWarpper(liuDeHua);
    // 调用装饰类的相关方法,完成方法扩展
    liuDeHuaWarpper.sing();
    liuDeHuaWarpper.dance();
}

commons-io工具包

commons-io工具包的概述

commons-io是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。commons-io工具包提供了很多有关io操作的类,见下表:

功能描述
org.apache.commons.io 有关Streams、Readers、Writers、Files的工具类
org.apache.commons.io.input 输入流相关的实现类,包含Reader和InputStream
org.apache.commons.io.output 输出流相关的实现类,包含Writer和OutputStream
org.apache.commons.io.serialization 序列化相关的类

commons-io工具包的使用

步骤:

  1. 下载commons-io相关jar包;http://commons.apache.org/proper/commons-io/
  2. 把commons-io-2.6.jar包复制到指定的Module的lib目录中
  3. 将commons-io-2.6.jar加入到classpath中

commons-io工具包的使用

  • commons-io提供了一个工具类 org.apache.commons.io.IOUtils,封装了大量IO读写操作的代码。其中有两个常用方法:
  1. public static int copy(InputStream in, OutputStream out); 把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以下)
  2. public static long copyLarge(InputStream in, OutputStream out);把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以上)

文件复制案例演示:

 // IOUtils工具类拷贝文件
    private static void method01() throws IOException {
        FileInputStream fis = new FileInputStream("day17\\aaa\\jdk11.exe");
        FileOutputStream fos = new FileOutputStream("day17\\aaa\\jdk11Copy4.exe");
        IOUtils.copy(fis,fos);
        fos.close();
        fis.close();
    }
  • commons-io还提供了一个工具类org.apache.commons.io.FileUtils,封装了一些对文件操作的方法:
  1. public static void copyFileToDirectory(final File srcFile, final File destFile) //复制文件到另外一个目录下。
  2. public static void copyDirectoryToDirectory( file1 , file2 );//复制file1目录到file2位置。

案例演示:

public static void main(String[] args) throws IOException {
		// FileUtils工具类拷贝文件到指定文件夹
        // File srcFile = new File("day17\\aaa\\a.txt");
        // File destFile = new File("day17\\eee");
        // FileUtils.copyFileToDirectory(srcFile,destFile);

        // FileUtils工具类拷贝文件夹到指定文件夹
        File srcFile = new File("day17\\ddd");
        File destFile = new File("day17\\eee");
        FileUtils.copyDirectoryToDirectory(srcFile,destFile);
    }

File

File类

File类的概述和构造方法

File类的概述

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

File类的构造方法

  • public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。

  • public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。

  • public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。

  • 构造举例,代码如下:


public class Test {
    public static void main(String[] args) {
        // 创建File对象表示‪H:\aaa\hb.jpg文件路径
        File f1 = new File("H:\\aaa\\hb.jpg");
        File f2 = new File("H:\\aaa","hb.jpg");
        File parent = new File("H:\\aaa");
        File f3 = new File(parent, "hb.jpg");
        System.out.println(f1);
        System.out.println(f2);
        System.out.println(f3);

        System.out.println("=========================================");
        // 创建File对象表示‪H:\aaa\2020文件夹路径
        File f4 = new File("H:\\aaa\\2020");
        File f5 = new File("H:\\aaa","2020");
        File f6 = new File(parent,"2020");
        System.out.println(f4);
        System.out.println(f5);
        System.out.println(f6);

        System.out.println("=========================================");
        // 路径不存在的
        File f7 = new File("H:\\aaa\\b.txt");// b.txt文件是不存在的
        File f8 = new File("H:\\aaa\\2018");// 2018文件夹是不存在的
        System.out.println(f7);
        System.out.println(f8);
    }
}

小贴士:

  1. 一个File对象代表硬盘中的一个文件或者目录。
  2. 无论该路径下是否存在文件或者目录,都不影响File对象的创建。

File类常用方法

绝对路径和相对路径

  • 绝对路径:从盘符开始的路径,这是一个完整的路径。
  • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。

public class Test {
    public static void main(String[] args) {
        /*
            绝对路径和相对路径:
                - 绝对路径:从盘符开始的路径,这是一个完整的路径。
                - 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。  掌握

            生活中:你在中粮商务公园2栋605    你对象在中粮商务公园
                你告诉你对象你在哪里?
                绝对路径:中国广东省深圳市宝安区留仙二路中粮商务公园2栋605
                相对路径:2栋605

            程序中:
               绝对路径:G:\szitheima104\day12\aaa\hb.jpg
               相对路径:day12\aaa\hb.jpg
         */
        // 绝对路径
        File f1 = new File("G:\\szitheima104\\day12\\aaa\\hb.jpg");

        // 相对路径
        File f2 = new File("day12\\aaa\\hb.jpg");

        System.out.println(f1);
        System.out.println(f2);
    }
}

获取功能的方法

  • public String getAbsolutePath() :返回此File的绝对路径名字符串。

  • public String getPath() :将此File转换为路径名字符串。

  • public String getName() :返回由此File表示的文件或目录的名称。

  • public long length() :返回由此File表示的文件的字节大小。 不能获取目录的字节大小。

    方法演示,代码如下:

    
    public class Test {
        public static void main(String[] args) {
    
            // 创建File对象,表示day12\aaa\hb.jpg文件的路径
            File f1 = new File("day12\\aaa\\hb.jpg");
            System.out.println("f1的绝对路径:"+f1.getAbsolutePath());// G:\szitheima104\day12\aaa\hb.jpg
            System.out.println("f1的构造路径:"+f1.getPath());// day12\aaa\hb.jpg
            System.out.println("f1的文件名:"+f1.getName());// hb.jpg
            System.out.println("f1的字节大小:"+f1.length());// 24,666 字节
    
            // 创建File对象,表示day12\aaa\bbb文件夹的路径
            File f2 = new File("day12\\aaa\\bbb");
            System.out.println("f2的绝对路径:"+f2.getAbsolutePath());// G:\szitheima104\day12\aaa\bbb
            System.out.println("f2的构造路径:"+f2.getPath());// day12\aaa\bbb
            System.out.println("f2的文件名:"+f2.getName());// bbb
            System.out.println("f2的字节大小:"+f2.length());// 0
        }
    }
    
    

API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。

判断功能的方法

  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。

方法演示,代码如下:


public class Test {
    public static void main(String[] args) {
        /*
            判断功能的方法:
                - public boolean exists() :此File表示的文件或目录是否实际存在。
                - public boolean isDirectory() :此File表示的是否为目录。
                - public boolean isFile() :此File表示的是否为文件。
         */
        // 路径真实存在
        // 创建File对象,表示day12\aaa\hb.jpg文件的路径
        File f1 = new File("day12\\aaa\\hb.jpg");
        System.out.println("f1表示的文件是否真实存在:"+f1.exists());// true
        System.out.println("f1表示的文件是否是文件夹:"+f1.isDirectory());// false
        System.out.println("f1表示的文件是否是文件:"+f1.isFile());// true

        System.out.println("=====================================================");

        // 路径真实存在
        // 创建File对象,表示day12\aaa\bbb文件夹的路径
        File f2 = new File("day12\\aaa\\bbb");
        System.out.println("f2表示的文件夹是否真实存在:"+f2.exists());// true
        System.out.println("f2表示的文件夹是否是文件夹:"+f2.isDirectory());// true
        System.out.println("f2表示的文件夹是否是文件:"+f2.isFile());// false

        System.out.println("=====================================================");
        // 注意:路径不真实存在,结果都是false
        File f3 = new File("day12\\aaa\\b.txt");
        System.out.println("f3表示的文件是否真实存在:"+f3.exists());// false
        System.out.println("f3表示的文件是否是文件夹:"+f3.isDirectory());// false
        System.out.println("f3表示的文件是否是文件:"+f3.isFile());// false
        File f4 = new File("day12\\aaa\\ccc");
        System.out.println("f4表示的文件夹是否真实存在:"+f4.exists());// false
        System.out.println("f4表示的文件夹是否是文件夹:"+f4.isDirectory());// false
        System.out.println("f4表示的文件夹是否是文件:"+f4.isFile());// false
    }
}

创建删除功能的方法

  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
  • public boolean delete() :删除由此File表示的文件或目录。
  • public boolean mkdir() :创建由此File表示的目录。
  • public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。

方法演示,代码如下:


public class Test {
    public static void main(String[] args) throws IOException {
        /*
            创建删除功能的方法:
                - public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
                - public boolean mkdir() :创建由此File表示的目录。
                - public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。
                - public boolean delete() :删除由此File表示的文件或目录。只能删除文件或者空文件夹,不能删除非空文件夹
         */
        // 创建文件:
        File f1 = new File("day12\\aaa\\a.txt");
        System.out.println("是否创建成功:"+f1.createNewFile());

        File f2 = new File("day12\\aaa\\ddd");// 创建ddd文件
        System.out.println("是否创建成功:"+f2.createNewFile());

        System.out.println("=======================================");

        // 创建文件夹
        File f3 = new File("day12\\aaa\\eee");
        System.out.println("是否创建成功:"+f3.mkdir());

        File f4 = new File("day12\\aaa\\fff.txt");// 创建fff.txt文件夹
        System.out.println("是否创建成功:"+f4.mkdir());

        File f5 = new File("day12\\aaa\\aaa\\bbb\\ccc\\ddd");
        System.out.println("是否创建成功:"+f5.mkdir());// 一定是false

        System.out.println("=======================================");
        System.out.println("是否创建成功:"+f5.mkdirs());

        System.out.println("=======================================");

        // 删除文件
        File f6 = new File("day12\\aaa\\a.txt");
        System.out.println("是否删除成功:"+f6.delete());// true

        // 删除空文件夹
        File f7 = new File("day12\\aaa\\eee");
        System.out.println("是否删除成功:"+f7.delete());// true

        // 删除非空文件夹
        File f8 = new File("day12\\aaa\\bbb");
        System.out.println("是否删除成功:"+f8.delete());// false

    }
}

API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。

File类遍历目录方法

  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录的名称。

  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录的路径。


public class Test {
    public static void main(String[] args) {
        /*
            File类遍历目录方法:
                - public String[] list() :返回一个String数组,表示该File目录中的所有子文件或子目录的名称。
                - public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或子目录的路径。
         */
        // 创建File对象,表示day12\aaa文件夹
        File f = new File("day12\\aaa");
        // 获取f路径下的所有子文件和子文件夹的名称
        String[] arr1 = f.list();
        // 循环遍历
        for (String s : arr1) {
            System.out.println(s);
        }

        System.out.println("==============================");

        // 获取f路径下的所有子文件和子文件夹的路径
        File[] arr2 = f.listFiles();
        // 循环遍历
        for (File file : arr2) {
            System.out.println(file);
        }

        System.out.println("==============================");

        // 注意:如果文件夹没有访问权限,那么返回的就是null,遍历就会报空指针异常
        File f1 = new File("H:\\System Volume Information");
        String[] list = f1.list();
        File[] files = f1.listFiles();
        System.out.println(list+","+files);// null,null
        // 报异常,为了代码的健壮性,所以循环之前加一个非空判断
        if (list != null) {
            for (String s : list) {

            }
        }

    }
}

小贴士:

调用listFiles方法的File对象,表示的必须是实际存在的目录,否则返回null,无法进行遍历。

递归

递归的概述

递归的概述

  • 生活中的递归: 放羊--赚钱--盖房子--娶媳妇--生娃--放羊--赚钱--盖房子--娶媳妇--生娃--放羊...
  • 程序中的递归: 指在当前方法内调用自己的这种现象。
  • 递归的注意事项:
    • 递归要有出口(结束方法),否则会报栈内存溢出错误StackOverflowError
    • 递归的出口不能太晚了

案例演示


public class Test {
    static int count = 0;
    public static void main(String[] args) {
        /*
            程序中的递归: 指在当前方法内调用自己的这种现象
            注意事项:
                1.递归一定要有出口,否则会报栈内存溢出错误StackOverflowError
                2.递归出口太晚了,还是会报栈内存溢出错误StackOverflowError
            解决办法:合理递归
         */
        method();
    }

    public static void method(){
        count++;
        if (count == 70000){
            return;
        }
        System.out.println("method 方法执行了...");
        method();
    }
}

递归累和

需求

  • 计算1 ~ n的累加和

分析

  • num的累加和 = num + (num-1)的累和,所以可以把累加和的操作定义成一个方法,递归调用。

实现

代码实现

public class Test1 {
    public static void main(String[] args) {
        /*
            练习一:使用递归计算1 ~ n的和
                分析:
                        1 的累加和 = 1                      1的累加和=1
                        2 的累加和 = 1 + 2                  2的累加和=2+1的累加和
                        3 的累加和 = 1 + 2 + 3              3的累加和=3+2的累加和
                        4 的累加和 = 1 + 2 + 3 + 4          4的累加和=4+3的累加和
                        .....
                        n 的累加和                          n的累加和=n+(n-1)的累加和
         */
        // 调用getSum方法计算5的累加和
        int sum = getSum(5);
        System.out.println("5的累加和:"+sum);// 15
    }

    /**
     * 计算一个数的累加和
     * @param n
     * @return
     */
    public static int getSum(int n){
        // 出口
        if(n == 1){
            return 1;
        }
        return n + getSum(n-1);// 规律
    }
}

代码执行图解

递归求阶乘

需求

  • 计算n的阶乘

分析

  • 阶乘:所有小于及等于该数的正整数的积。
n的阶乘:n! = n * (n-1) *...* 3 * 2 * 1 

n的阶乘 = n * (n1)的阶乘,所以可以把阶乘的操作定义成一个方法,递归调用。

推理得出:n! = n * (n-1)!

实现

代码实现

public class Test2 {
    public static void main(String[] args) {
        /*
            递归求阶乘:
                规律:
                    1! = 1                                      1的阶乘 : 1
                    2! = 2 * 1                                  2的阶乘 : 2 * 1的阶乘
                    3! = 3 * 2 * 1                              3的阶乘 : 3 * 2的阶乘
                    4! = 4 * 3 * 2 * 1                          4的阶乘 : 4 * 3的阶乘
                    5! = 5 * 4 * 3 * 2 * 1                      5的阶乘 : 5 * 4的阶乘
                    ....
                    num! = num * (num-1) * (num-2) *...* 1      num的阶乘 : num * num-1的阶乘
         */
        int res = jieCheng(5);
        System.out.println("5的阶乘:"+res);// 5的阶乘:120
    }

    /**
     * 计算一个数的阶乘
     * @param num
     * @return
     */
    public static int jieCheng(int num){
        // 出口
        if (num == 1){
            return 1;
        }
        return num *  jieCheng(num-1); // 计算阶乘的规律
    }
}

文件搜索

需求

  • 递归输出day15\src目录中的所有.java文件的绝对路径。

分析

  1. 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
  2. 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。

实现

public class Test3_文件搜索 {
    public static void main(String[] args) {
        /*
            需求:
                输出day15\\src目录中的所有.java文件的绝对路径。
            分析:
                1.定义一个方法,用来获取一个目录中所有符合条件的文件(子文件,子子文件,子子子文件...)
                2.在方法中,获取该目录下的所有子文件和子目录
                3.在方法中,循环遍历获取到的所有字文件和子目录
                4.在方法中,遍历的时候,需要判断遍历出来的是文件还是目录
                5.如果是文件,就判断该文件是否以.java结尾,如果是就获取其绝对路径打印输出
                6.如果是文件夹,就递归调用该方法
         */
        File file = new File("day15\\src");
        findFile(file);
    }

    /**
     * 定义一个方法,用来获取一个目录中所有符合条件的文件(子文件,子子文件,子子子文件...)
     * @param file
     */
    public static void findFile(File file) {
        // 在方法中,获取该目录下的所有子文件和子目录
        File[] files = file.listFiles();

        // 在方法中,循环遍历获取到的所有字文件和子目录
        if (files != null) {
            // 在方法中,遍历的时候,需要判断遍历出来的是文件还是目录
            for (File file1 : files) {
                // 如果是文件,就判断该文件是否以.java结尾,如果是就获取其绝对路径打印输出
                if (file1.isFile() && file1.getName().endsWith(".java")){
                    System.out.println(file1.getAbsolutePath());
                }
                // 如果是文件夹,就递归调用该方法
                if (file1.isDirectory()){
                    findFile(file1);
                }
            }
        }
    }

}

Socket编程

网络编程入门

软件结构

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。
  • 特点: 客户端和服务器是分开的,需要下载客户端,对网络要求相对低, 服务器压力小,开发和维护成本高,相对稳定

1566446300784

B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。

特点:没有客户端,只有服务器,不需要下载客户端,直接通过浏览器访问, 对网络要求相对高, 服务器压力很大,相对不稳定,开发和维护成本低,

1566446315067

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机在网络中的通信的程序。

网络编程三要素

协议

网络通信协议:通信协议是计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守,最终完成数据交换。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
  • TCP协议特点: 面向连接,传输数据安全,传输速度低
  • 例如: 村长发现张三家的牛丢了
  • TCP协议: 村长一定要找到张三,面对面的告诉他他家的牛丢了 打电话: 电话一定要接通,并且是张三接的
    • 连接三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
      • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。 你愁啥?
      • 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。我愁你咋地?
      • 第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。你再愁试试

image-20220726134219054

​ 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等

  • UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中,例如视频会议、QQ聊天等。
  • UDP特点: 面向无连接,传输数据不安全,传输速度快
  • 例如: 村长发现张三家的牛丢了
  • UDP协议: 村长在村里的广播站广播一下张三家的牛丢了,信息丢失,信息发布速度快

IP地址

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

**IP地址分类 **

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有资料显示,全球IPv4地址在2011年2月分配完毕。

    为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

常用命令

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

特殊的IP地址

  • 本机IP地址:127.0.0.1localhost

端口号

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

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

  • 端口号:用两个字节表示的整数,它的取值范围是065535**。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。**

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

小结

  • 协议: 计算机在网络中通信需要遵守的规则,常见的有TCP,UDP协议
    • TCP: 面向连接,传输数据安全,传输速度慢
    • UDP: 面向无连接,传输不数据安全,传输速度快
  • IP地址: 用来标示网络中的计算机设备
    • 分类: IPV4 IPV6
    • 本地ip地址: 127.0.0.1 localhost
  • 端口号: 用来标示计算机设备中的应用程序
    • 端口号: 0--65535
    • 自己写的程序指定的端口号要是1024以上
    • 如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

InetAddress类

InetAddress类的概述

  • 一个该类的对象就代表一个IP地址对象。

InetAddress类的方法

  • static InetAddress getLocalHost() 获得本地主机IP地址对象
  • static InetAddress getByName(String host) 根据IP地址字符串或主机名获得对应的IP地址对象
  • String getHostName();获得主机名
  • String getHostAddress();获得IP地址字符串
public class InetAddressDemo01 {
    public static void main(String[] args) throws Exception {
              // 获取本地ip地址对象
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println("ip1: "+ip1);// DESKTOP-U8Q5F96/192.168.0.100

        // 获取百度ip地址对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println("ip2:"+ip2);// www.baidu.com/182.61.200.7

        // 获得本地的主机名
        String hostName = ip1.getHostName();
        System.out.println("hostName:"+hostName);// DESKTOP-U8Q5F96

        // 获得本地的ip地址
        String hostAddress = ip1.getHostAddress();
        System.out.println("hostAddress:"+hostAddress);//192.168.0.100
    }
}

TCP通信程序

TCP协议概述

TCP概述

  • TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收器端建立逻辑连接,然后再传输数据。它提供了两台计算机之间可靠无差错的数据传输。TCP通信过程如下图所示:

1566446503937

TCP协议相关的类

  • Socket : 一个该类的对象就代表一个客户端程序。
    • Socket(String host, int port) 根据ip地址字符串和端口号创建客户端Socket对象
      * 注意事项:只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。
      如果连接成功,则表示三次握手通过。
    • OutputStream getOutputStream(); 获得字节输出流对象
    • InputStream getInputStream();获得字节输入流对象
    • void close();关闭Socket, 会自动关闭相关的流,关闭通过Socket获得流,也会关闭Socket
  • ServerSocket : 一个该类的对象就代表一个服务器端程序。
    • ServerSocket(int port); 根据指定的端口号开启服务器。
    • Socket accept(); 等待客户端连接并获得与客户端关联的Socket对象 如果没有客户端连接,该方法会一直阻塞
    • void close();关闭ServerSocket,一般不关闭

TCP通信案例1

需求

  • 客户端向服务器发送字符串数据

路径

  • 客户端实现步骤
    • 创建客户端Socket对象并指定服务器地址和端口号
    • 调用Socket对象的getOutputStream方法获得字节输出流对象
    • 使用字节输出流对象的write方法往服务器端输出数据
    • 关闭Socket对象断开连接。
  • 服务器实现步骤
    • 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
    • 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
    • 调用Socket对象的getInputStream方法获得字节输入流对象
    • 调用字节输入流对象的read方法读取客户端发送的数据

实现

  • 客户端代码实现

    
    public class Client {
        public static void main(String[] args) throws Exception{
            // 客户端:
            // 1.创建Socket对象,指定要连接的服务器的ip地址和端口号
            Socket socket = new Socket("127.0.0.1",6666);
            System.out.println(socket);// 封装了服务器的ip地址和端口号
    
            // 2.通过Socket对象获得输出流
            OutputStream os = socket.getOutputStream();
    
            // 3.写出数据到服务器
            os.write("服务器,你好,今晚约吗?".getBytes());
    
            // 4.关闭流,释放资源
            socket.close();
    
        }
    }
    
    
  • 服务端代码实现

    
    public class Server {
        public static void main(String[] args) throws Exception{
            // 服务器:
            // 1.创建ServerSocket对象,指定服务器的端口号(6666)
            ServerSocket ss = new ServerSocket(6666);
    
            // 2.调用accept()方法,接收客户端的请求,返回Socket对象
            Socket socket = ss.accept();
            System.out.println(socket);// 封装了客户端的ip地址和端口号
    
            // 3.使用返回的Socket对象获得输入流
            InputStream is = socket.getInputStream();
    
            // 4.读取客户端写过来的数据
            byte[] bys = new byte[8192];
            int len = is.read(bys);
            System.out.println(new String(bys,0,len));
    
            // 5.关闭服务器(一般不关闭)
            ss.close();
    
        }
    }
    
    

TCP通信案例2

需求

  • 客户端向服务器发送字符串数据,服务器回写字符串数据给客户端(模拟聊天)

路径

  • 客户端实现步骤
    • 创建客户端Socket对象并指定服务器地址和端口号
    • 调用Socket对象的getOutputStream方法获得字节输出流对象
    • 使用字节输出流对象的write方法往服务器端输出数据
    • 调用Socket对象的getInputStream方法获得字节输入流对象
    • 调用字节输入流对象的read方法读取服务器端返回的数据
    • 关闭Socket对象断开连接。
  • 服务器实现步骤
    • 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
    • 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
    • 调用Socket对象的getInputStream方法获得字节输入流对象
    • 调用字节输入流对象的read方法读取客户端发送的数据
    • 调用Socket对象的getOutputStream方法获得字节输出流对象
    • 调用字节输出流对象的write方法往客户端输出数据
    • 关闭Socket和ServerSocket对象

实现

  • TCP客户端代码
/*
TCP客户端代码实现步骤
        * 创建客户端Socket对象并指定服务器地址和端口号
        * 调用Socket对象的getOutputStream方法获得字节输出流对象
        * 调用字节输出流对象的write方法往服务器端输出数据
        * 调用Socket对象的getInputStream方法获得字节输入流对象
        * 调用字节输入流对象的read方法读取服务器端返回的数据
        * 关闭Socket对象断开连接。
*/
public class Client {
    public static void main(String[] args) throws Exception{
        // 创建Socket对象,指定服务器ip和端口号
        Socket socket = new Socket("127.0.0.1",6666);
        while (true) {
            // 通过socket对象获得输出流
            OutputStream os = socket.getOutputStream();
            // 写出数据
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入向服务器发送的数据:");
            String str = sc.nextLine();
            os.write(str.getBytes());

            // 通过Socket对象获得输入流
            InputStream is = socket.getInputStream();
            // 定义一个byte数组,用来存储读取到的字节数据
            byte[] bys = new byte[1024];
            int len = is.read(bys);
            // 打印数据
            System.out.println(new String(bys,0,len));
        }

        // 关闭流,释放资源
        //socket.close();
    }
}
  • 服务端代码实现
/**
    TCP服务器端代码实现步骤
        * 创建ServerSocket对象并指定端口号(相当于开启了一个服务器)
        * 调用ServerSocket对象的accept方法等待客端户连接并获得对应Socket对象
        * 调用Socket对象的getInputStream方法获得字节输入流对象
        * 调用字节输入流对象的read方法读取客户端发送的数据
        * 调用Socket对象的getOutputStream方法获得字节输出流对象
        * 调用字节输出流对象的write方法往客户端输出数据
        * 关闭Socket和ServerSocket对象
 */
public class Server {
    public static void main(String[] args) throws Exception{
        // 创建ServerSocket对象,并指定端口号
        ServerSocket ss = new ServerSocket(6666);
        // 调用accept()方法等待客户端连接,连接成功返回Socket对象
        Socket socket = ss.accept();

        while (true) {
            // 通过Socket对象获得输入流
            InputStream is = socket.getInputStream();
            // 定义一个byte数组,用来存储读取到的字节数据
            byte[] bys = new byte[1024];
            int len = is.read(bys);
            // 打印数据
            System.out.println(new String(bys,0,len));

            // 通过socket对象获得输出流
            OutputStream os = socket.getOutputStream();
            // 写出数据
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入向客户端发送的数据:");
            String str = sc.nextLine();
            os.write(str.getBytes());
        }

        // 关闭资源
        //socket.close();
        //ss.close();// 服务器一般不关闭
    }
}

综合案例

文件上传案例

需求

  • 使用TCP协议, 通过客户端向服务器上传一个文件

分析

  1. 【客户端】输入流,从硬盘读取文件数据到程序中。

  2. 【客户端】输出流,写出文件数据到服务端。

  3. 【服务端】输入流,读取文件数据到服务端程序。

  4. 【服务端】输出流,写出文件数据到服务器硬盘中。

  5. 【服务端】获取输出流,回写数据。

  6. 【客户端】获取输入流,解析回写数据。

1566446548503

实现

拷贝文件

public class Client {
    public static void main(String[] args) throws Exception {
        // 1.创建输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\aaa\\hb.jpg");

        // 2.创建Socket对象,指定要连接的服务器的ip地址和端口号
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);

        // 3.通过Socket对象获得输出流,关联连接通道
        OutputStream os = socket.getOutputStream();

        // 4.定义变量,用来存储读取到的字节数据
        byte[] bys = new byte[8192];
        int len;

        // 5.循环读取
        while ((len = fis.read(bys)) != -1) {
            // 6.在循环中,写出数据到通道中
            os.write(bys,0,len);
        }
        // 7.释放资源
        socket.close();
        fis.close();
    }
}
public class Server {
    public static void main(String[] args) throws Exception {
        // 1.创建ServerSocket对象,指定端口号 8888
        ServerSocket ss = new ServerSocket(8888);

        // 2.使用ServerSocket对象调用accept()方法,接收请求,建立连接,返回Socket对象
        Socket socket = ss.accept();

        // 3.通过返回的Socket对象获得输入流,关联连接通道
        InputStream is = socket.getInputStream();

        // 4.创建输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\aaa\\hbCopy2.jpg");

        // 5.定义变量,用来存储读取到的字节数据
        byte[] bys = new byte[8192];
        int len;

        // 6.循环读取
        while ((len = is.read(bys)) != -1) {
            // 7.在循环中,写出数据目的文件中
            fos.write(bys,0,len);
        }
        // 8.释放资源
        fos.close();
        socket.close();
    }
}

文件上传成功后服务器回写字符串数据

// 客户端
public class Client {
    public static void main(String[] args) throws Exception {
        // 1.创建输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\aaa\\hb.jpg");

        // 2.创建Socket对象,指定要连接的服务器的ip地址和端口号
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);

        // 3.通过Socket对象获得输出流,关联连接通道
        OutputStream os = socket.getOutputStream();

        // 4.定义变量,用来存储读取到的字节数据
        byte[] bys = new byte[8192];
        int len;

        // 5.循环读取
        while ((len = fis.read(bys)) != -1) {
            // 6.在循环中,写出数据到通道中
            os.write(bys, 0, len);
        }

        // - 在文件上传时,客户端从文件中读不到数据,就会停止发送。
        // 但是服务器端不知道客户端停止了,所以会一直等待接收数据。
		// 解决办法:在客户端调用s.shutdownOutput();通知服务器端发送结束了。
        socket.shutdownOutput();// 注意

        System.out.println("============客户端开始接受服务器返回的数据==============");
        // 7.通过Socket对象获取输入流,关联连接通道
        InputStream is = socket.getInputStream();

        // 8.读取服务器回写的数据
        int read = is.read(bys);// 卡死  读取服务器写回的数据,但是服务器又没有写回数据
        // 9.打印服务器回写的数据
        System.out.println("服务器回写的数据是:" + new String(bys, 0, read));

        // 10.释放资源
        socket.close();
        fis.close();
    }
}
// 服务器
public class Server {
    public static void main(String[] args) throws Exception {
        // 1.创建ServerSocket对象,指定端口号 8888
        ServerSocket ss = new ServerSocket(8888);

        // 2.使用ServerSocket对象调用accept()方法,接收请求,建立连接,返回Socket对象
        Socket socket = ss.accept();

        // 3.通过返回的Socket对象获得输入流,关联连接通道
        InputStream is = socket.getInputStream();

        // 4.创建输出流对象,关联目的地文件路径
        FileOutputStream fos = new FileOutputStream("day12\\aaa\\hbCopy5.jpg");

        // 5.定义变量,用来存储读取到的字节数据
        byte[] bys = new byte[8192];
        int len;

        // 6.循环读取
        while ((len = is.read(bys)) != -1) {
            // 7.在循环中,写出数据目的文件中
            fos.write(bys, 0, len);
        }

        System.out.println("======服务器开始回写数据给客户端=======");
        // 7.通过socket对象获取输出流,关联连接通道
        OutputStream os = socket.getOutputStream();

        // 8.写出数据到通道中
        os.write("恭喜您,上传成功!".getBytes());

        // 9.释放资源
        fos.close();
        socket.close();
    }
}

优化文件上传案例

1.文件名固定----->优化   自动生成唯一的文件名
2.服务器只能接受一次 ----> 优化  死循环去接收请求,建立连接
3.例如:如果张三先和服务器建立连接,上传了一个2GB字节大小的文件
       李四后和服务器建立连接,上传了一个2MB字节大小的文件

       李四就必须等张三上传完毕,才能上传文件

   优化---->多线程优化
   张三上传文件,开辟一条线程
   李四上传文件,开辟一条线程
// 服务器
public class Server {
    // year+"年"+month+"月"+day+"日"+....+"毫秒"
    public static void main(String[] args) throws Exception {
        // 1.创建ServerSocket对象,指定端口号 8888
        ServerSocket ss = new ServerSocket(8888);

        while (true) {
            // 2.使用ServerSocket对象调用accept()方法,接收请求,建立连接,返回Socket对象
            Socket socket = ss.accept();

           new Thread(new Runnable() {
               @Override
               public void run() {

                   try {
                       // 3.通过返回的Socket对象获得输入流,关联连接通道
                       InputStream is = socket.getInputStream();

                       // 4.创建输出流对象,关联目的地文件路径
                       FileOutputStream fos = new FileOutputStream("day12\\aaa\\"+System.currentTimeMillis()+".jpg");

                       // 5.定义变量,用来存储读取到的字节数据
                       byte[] bys = new byte[8192];
                       int len;

                       // 6.循环读取
                       while ((len = is.read(bys)) != -1) {
                           // 7.在循环中,写出数据目的文件中
                           fos.write(bys, 0, len);
                       }

                       System.out.println("======服务器开始回写数据给客户端=======");
                       // 7.通过socket对象获取输出流,关联连接通道
                       OutputStream os = socket.getOutputStream();

                       // 8.写出数据到通道中
                       os.write("恭喜您,上传成功!".getBytes());

                       // 9.释放资源
                       fos.close();
                       socket.close();

                   } catch (IOException e) {

                   }
               }
           }).start();

        }
    }
}
// 客户端
public class Client {
    public static void main(String[] args) throws Exception {
        // 1.创建输入流对象,关联数据源文件路径
        FileInputStream fis = new FileInputStream("day12\\aaa\\hb.jpg");

        // 2.创建Socket对象,指定要连接的服务器的ip地址和端口号
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);

        // 3.通过Socket对象获得输出流,关联连接通道
        OutputStream os = socket.getOutputStream();

        // 4.定义变量,用来存储读取到的字节数据
        byte[] bys = new byte[8192];
        int len;

        // 5.循环读取
        while ((len = fis.read(bys)) != -1) {
            // 6.在循环中,写出数据到通道中
            os.write(bys, 0, len);
        }

        // 想办法,告诉服务器,我客户端写完了数据,我再也不会写数据了
        socket.shutdownOutput();// 注意

        System.out.println("============客户端开始接受服务器返回的数据==============");
        // 7.通过Socket对象获取输入流,关联连接通道
        InputStream is = socket.getInputStream();

        // 8.读取服务器回写的数据
        int read = is.read(bys);
        // 9.打印服务器回写的数据
        System.out.println("服务器回写的数据是:" + new String(bys, 0, read));

        // 10.释放资源
        socket.close();
        fis.close();
    }
}

模拟B\S服务器 扩展

需求

  • 模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。

分析

  1. 准备页面数据,web文件夹。
  2. 我们模拟服务器端,ServerSocket类监听端口,使用浏览器访问,查看网页效果

实现

浏览器工作原理是遇到图片会开启一个线程进行单独的访问,因此在服务器端加入线程技术。

public class Demo {
    public static void main(String[] args) throws Exception {
        // 通过读取浏览器端的请求信息,获取浏览器需要访问的页面的路径
        // 1.创建ServerSocket对象,指定端口号为9999
        ServerSocket ss = new ServerSocket(9999);

        while (true) {
            // 2.调用accept()方法,接收请求,建立连接,返回Socket对象
            Socket socket = ss.accept();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 3.通过返回的Socket对象获取字节输入流,关联连接通道
                        InputStream is = socket.getInputStream();

                        // 4.把字节输入流转换为字符输入流
                        InputStreamReader isr = new InputStreamReader(is);

                        // 5.创建字符缓冲输入流
                        BufferedReader br = new BufferedReader(isr);

                        // 6.使用字符缓冲输入流读取第一行数据
                        String line = br.readLine();

                        // 7.使用空格对读取到的第一行数据进行分割
                        String[] arr = line.split(" ");

                        // 8.获取分割后数组中索引为1的元素,对其进行截取
                        String path = arr[1].substring(1);
                        System.out.println("浏览器需要访问的页面路径是:" + path);

                        // 服务器把浏览器需要访问的页面响应给浏览器
                        // 9.创建一个字节输入流,关联数据源文件路径
                        FileInputStream fis = new FileInputStream(path);

                        // 10.通过Socket对象获得输出流,关联连接通道
                        OutputStream os = socket.getOutputStream();

                        // 11.定义一个变量,用来存储读取到的字节数据
                        byte[] bys = new byte[8192];
                        int len;

                        // 响应页面的时候需要同时把以下响应过去给浏览器
                        os.write("HTTP/1.1 200 OK\r\n".getBytes());
                        os.write("Content-Type:text/html\r\n".getBytes());
                        os.write("\r\n".getBytes());


                        // 12.循环读取
                        while ((len = fis.read(bys)) != -1) {
                            // 13.在循环中,写出数据给浏览器
                            os.write(bys, 0, len);
                        }

                        // 关闭Socket对象,释放资源
                        fis.close();
                        socket.close();
                    } catch (IOException e) {

                    }
                }
            }).start();
        }
    }

    /**
     * 1.读取到浏览器端的请求信息
     *
     * @return
     * @throws IOException
     */
    private static Socket method01() throws IOException {
        //  1.读取到浏览器端的请求信息
        // 1.1 创建ServerSocket对象,指定端口号为9999
        ServerSocket ss = new ServerSocket(9999);
        // 1.2 调用accept()方法,接收请求,建立连接,返回Socket对象
        Socket socket = ss.accept();
        // 1.3 通过返回的Socket对象获取输入流,关联连接通道
        InputStream is = socket.getInputStream();
        // 1.4 使用输入流去读取数据
        byte[] bys = new byte[8192];
        int len = is.read(bys);
        // 1.5 打印读取到的数据
        System.out.println(new String(bys, 0, len));
        /*
            GET /day12/web/index.html HTTP/1.1
            Host: localhost:9999
            Connection: keep-alive
            Cache-Control: max-age=0
            Upgrade-Insecure-Requests: 1
            User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
            Sec-Fetch-User: ?1
            Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,**;q=0.8,application/signed-exchange;v=b3
                    Sec-Fetch-Site: none
                    Sec-Fetch-Mode: navigate
                    Accept-Encoding: gzip, deflate, br
                    Accept-Language: zh-CN,zh;q=0.9
                    Cookie: Idea-7071e3d8=cc177568-5581-4562-aeac-26fcb6ca7e56
         */
        return socket;
    }
}

访问效果:

1566446578300

图解:

1566446643154

Junit测试

Junit单元测试

Junit的概念

  • 概述 : Junit是Java语言编写的第三方单元测试框架(工具类)
  • 作用 : 用来做“单元测试”——针对某个普通方法,可以像main()方法一样独立运行,它专门用于测试某个方法。

Junit的使用步骤

  • 1.在模块下创建lib文件夹,把Junit的jar包复制到lib文件夹中

  • 2.选中Junit的jar包,右键选中 add as Library,把JUnit4的jar包添加到classPath中

maven 引入依赖

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
  • 3.在测试方法上面写上@Test注解

  • 4.执行测试方法

    public class Person {
    
        @Test
        public void test1(){
            System.out.println("Person test1 方法执行了....");
        }
    
        @Test
        public void test2(){
            System.out.println("Person test2 方法执行了....");
        }
    
    }
    
    

执行测试方法

  • 1.选中方法名--->右键--->选中执行 只执行选中的测试方法

  • 2.选中类名----->右键--->选中执行 执行该类中所有的测试方法

  • 3.选中模块---- ->右键--->选中all tests 执行 执行该模块中所有的测试方法

  • 如何查看测试结果

    • 绿色:表示测试通过
    • 红色:表示测试失败,有问题

Junit单元测试的注意实现

  • 1.测试方法的权限修饰符一定是public
  • 2.测试方法的返回值类型一定是void
  • 3.测试方法一定没有参数
  • 4.测试方法 的声明之上一定要使用@Test注解

Junit注解

  • @Test:测试注释指示该公共无效方法它所附着可以作为一个测试用例。
  • @Before:用来修饰方法,该方法会在每一个测试方法执行之前执行一次。
  • @After:用来修饰方法,该方法会在每一个测试方法执行之后执行一次。(如执行每一个测试后重置某些变量,删除临时变量等)
  • @BeforeClass:用来静态修饰方法,该方法会在所有测试方法之前执行一次,而且只执行一次。(一般是测试计算共享配置方法(如连接到数据库)。)
  • @AfterClass:用来静态修饰方法,该方法会在所有测试方法之后执行一次,而且只执行一次。(AfterClass 注解可以使用以清理建立方法,(从数据库如断开连接)。)
  • @Ignore:当想暂时禁用特定的测试执行可以使用忽略注释。每个被注解为 @Ignore 的方法将不被执行。
public class Student {

    @BeforeClass
    public static void beforeClass1(){
        System.out.println("Student beforeClass1静态方法执行了...");
    }

    @BeforeClass
    public static void beforeClass2(){
        System.out.println("Student beforeClass2静态方法执行了...");
    }


    @Before
    public void b1(){
        System.out.println("Student b1方法执行了...");
    }

    @Before
    public void b2(){
        System.out.println("Student b2方法执行了...");
    }

    @Before
    public void b3(){
        System.out.println("Student b3方法执行了...");
    }


    @Test
    public void test1(){
        System.out.println("Student test1 方法执行了....");
    }

    @Test
    public void test2(){
        System.out.println("Student test2 方法执行了....");
    }

    @After
    public void a1(){
        System.out.println("Student a1方法执行了...");
    }

    @After
    public void a2(){
        System.out.println("Student a2方法执行了...");
    }

    @After
    public void a3(){
        System.out.println("Student a3方法执行了...");
    }

    @AfterClass
    public static void afterClass1(){
        System.out.println("Student afterClass1方法执行了...");
    }

    @AfterClass
    public static void afterClass2(){
        System.out.println("Student afterClass2方法执行了...");
    }

}

Junit断言

断言是编程术语,表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。

  • 断言:预先判断某个条件一定成立,如果条件不成立,则直接报错。 使用Assert类中的assertEquals()方法

使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为 false 时中断当前操作的话,可以使用断言。单元测试必须使用断言(Junit/JunitX)。

常用断言方法

断言 描述
void assertEquals([String message], expected value, actual value) 断言两个值相等。值可能是类型有 int, short, long, byte, char or java.lang.Object. 第一个参数是一个可选的字符串消息
void assertTrue([String message], boolean condition) 断言一个条件为真
void assertFalse([String message],boolean condition) 断言一个条件为假
void assertNotNull([String message], java.lang.Object object) 断言一个对象不为空(null)
void assertNull([String message], java.lang.Object object) 断言一个对象为空(null)
void assertSame([String message], java.lang.Object expected, java.lang.Object actual) 断言,两个对象引用相同的对象
void assertNotSame([String message], java.lang.Object unexpected, java.lang.Object actual) 断言,两个对象不是引用同一个对象
void assertArrayEquals([String message], expectedArray, resultArray) 断言预期数组和结果数组相等。数组的类型可能是 int, long, short, char, byte or java.lang.Object.

案例

public class Demo02 {
    @Test
    public void addTest(){
        //测试
        int add = add(3, 6);

        //断言判断结果
        //第一个参数表示期望值
        //第二个参数表示实际值
        //如果结果正确的就测试通过,如果结果错误的,就会报错
        Assert.assertEquals(9,add);
    }

    //加法
    //这个代码的语法没问题,也没有异常。他是逻辑错误,系统不知道你要算的是加法
    public int add(int a, int b){
        int sum = a * b;
        return sum;
    }
    /**
	* 测试断言
	*/
    @Test
    public void testAssert() {
        String obj1 = "junit";
        String obj2 = "junit";
        String obj3 = "test";
        String obj4 = "test";
        String obj5 = null;
        int var1 = 1;
        int var2 = 2;
        int[] arithmetic1 = {1, 2, 3};
        int[] arithmetic2 = {1, 2, 3};
        assertEquals(obj1, obj2);
        assertSame(obj3, obj4);
        assertNotSame(obj2, obj4);
        assertNotNull(obj1);
        assertNull(obj5);
        assertTrue("为真", var1 == var2);
        assertArrayEquals(arithmetic1, arithmetic2);
    }
}

扩展

测试本身也是一套完整学科

单元测试
        白盒测试        能看到完整代码的
        黑盒测试         没有源码的,功能测试
        灰盒测试
压力测试
        并发数的问题,能承载多少并发
疲劳强度测试
        长期稳定运行,72小时,7天
冒烟测试
        疯狂测试一个点
        对主要流程测试,如支付环节
集成测试
        完整功能的测试,最重要的是测试,整体业务流程
回归测试
        增加一个功能,走集成测试,但是可能只有测主业务
自动化测试
        编码,场景设计

日志

日志框架可以说是软件系统的标配,平时使用日志的时候多数只是了解日志配置怎么配,对于这些日志框架的不同以及他们之间的关系很少了解。Java中的日志体系可以用杂乱无章来形容,因为很多,今天就来梳理下这些日志框架到底是怎么回事。

还在为弄不清commons-logging-xx.jarlog4j-xx.jarsl4j-api-xx.jar等日志框架之间复杂的关系而感到烦恼吗?

还在为如何统一系统的日志输出而感到不知所措嘛?

您是否依然存在这样的烦恼。比如,要更改spring的日志输出为log4j 2,却不知该引哪些jar包,只知道去百度一下所谓的博客,照着人家复制,却无法弄懂其中的原理?

不要急,不要方!本文带你们弄懂其中的原理,只要你静下心看本文,你就能随心所欲更改你系统里的日志框架,统一日志输出!

Log4j

Log4j 概述

一个完整的软件,日志是必不可少的。程序从开发、测试、维护、运行等环节,都需要向控制台或文件等位置输出大量信息。这些信息的输出, 在很多时候是使用 System.out.println() 无法完成的。

日志信息根据用途与记录内容的不同,分为 调试日志、运行日志、异常日志 等。

Log4j 的全称为 Log for java,即专门用于 Java 语言的日志记录工具。

对log4j的定位

用log4j,日志管理,是为了代替 sout输出语句,减少系统资源的开销,报错CPU资源,内存资源。

架构思想有些过时了,单体应用时用一用。

到SpringBoot时学logback

Log4j 日志级别

为了方便对于日志信息的输出显示,对日志内容进行了分级管理。日志级别由高到低,共分 6 个级别:

  • fatal(致命的):非常验证的错误,一般是系统错误
  • error:错误,一般表示代码错误,比较严重
  • warn:警告,不影响程序的运行,但是可能存在风险
  • info:信息,表示一个普通的输出信息
  • debug:调试,表示程序员认为的一些调试信息
  • trace(堆栈)

为什么要对日志进行分级

无论是将日志输出到控制台,还是文件,其输出都会降低程序的运行效率。但由于调试、运行维护的需要,客户的要求等原因,需要进行必要的日志输出。这时就必须要在代码中加入日志输出语句。

这些输出语句若在程序运行时全部执行, 则势必会降低运行效率。例如, 使用 System.out.println() 将信息输出到控制台,则所有的该输出语句均将执行。会大大降低程序的执行效率。而要使其不输出,唯一的办法就是将这些输出语句逐个全部删除。这是个费时费力的过程。

将日志信息进行分级管理,便可方便的控制信息输出内容及输出位置:哪些信息需要输出,哪些信息不需要输出,只需在一个日志输出控制文件中稍加修改即可。而代码中的输出语句不用做任何修改。

从这个角度来说,代码中的日志编写,其实就是写大量的输出语句。只不过,这些输出语句比较特殊,它们具有级别,在程序运行期间不一定被执行。它们的执行是由另一个控制文件控制。

Log4j 日志输出控制文件

日志输出简介

Log4j 的日志输出控制文件,主要由三个部分构成:

  • 日志信息的输出位置:控制日志信息将要输出的位置,是控制台还是文件等。
  • 日志信息的输出格式:控制日志信息的显示格式,即以怎样的字符串形式显示。
  • 日志信息的输出级别:控制日志信息的显示内容,即显示哪些级别的日志信息。

有了日志输出控制文件,代码中只要设置好日志信息内容及其级别即可,通过控制文件便可控制这些日志信息的输出了。

日志属性配置文件

日志属性文件 log4j.properties 是专门用于控制日志输出的。其主要进行三方面控制:

  • 输出位置:控制日志将要输出的位置,是控制台还是文件等。
  • 输出布局:控制日志信息的显示形式。
  • 输出级别:控制要输出的日志级别。

日志属性文件由两个对象组成:日志附加器与根日志。

根日志,即为 Java 代码中的日志记录器,其主要由两个属性构成:日志输出级别与日志附加器。

日志附加器,则由日志输出位置定义,由其它很多属性进行修饰,如输出布局、文件位置、文件大小等。

什么是日志附加器?

所谓日志附加器,就是为日志记录器附加上很多其它设置信息。附加器的本质是一个接口,其定义语法为:log4j.appender.appenderName = 输出位置

常用的附加器实现类
  • org.apache.log4j.ConsoleAppender
  • org.apache.log4j.FileAppender
  • org.apache.log4j.RollingFileAppender
  • org.apache.log4j.DailyRollingFileAppender

常用布局类型

  • org.apache.log4j.HTMLLayout
  • org.apache.log4j.SimpleLayout
  • org.apache.log4j.PatternLayout

打印参数: Log4J 采用类似 C 语言中的 printf 函数的打印格式格式化日志信息

  • %m
  • %p
  • %r
  • %c
  • %t
  • %n
  • %d
  • %l

图解:

0eeb444aa805797f184b9b0338f29639.png

第一个 Log4j 日志文件

Slf4j 简介

slf4j 的全称是 Simple Loging Facade For Java,即它仅仅是一个为 Java 程序提供日志输出的统一接口,并不是一个具体的日志实现方案,就比如 JDBC 一样,只是一种规则而已。所以单独的 slf4j 是不能工作的,必须搭配其他具体的日志实现方案,比如 apache 的 org.apache.log4j.Logger,JDK 自带的 java.util.logging.Logger 以及 log4j 等。

门面模式facader,slf4j,接口调用具体怎么实现我不管

POM

继续之前的项目,pom.xml 配置如下:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.17.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>
</dependencies>

主要增加了 org.slf4j:slf4j-log4j12 依赖

创建 log4j.properties 配置文件

在 src/main/resources 目录下创建名为 log4j.properties 的属性配置文件

log4j.rootLogger=INFO, console, file

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d %p [%c] - %m%n

log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=logs/log.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.A3.MaxFileSize=1024KB
log4j.appender.A3.MaxBackupIndex=10
log4j.appender.file.layout.ConversionPattern=%d %p [%c] - %m%n

日志配置相关说明:

  • log4j.rootLogger
  • log4j.appender.console
  • log4j.appender.console.layout
  • log4j.appender.console.layout.ConversionPattern
  • log4j.appender.file
  • log4j.appender.file.File
  • log4j.appender.file.layout
  • log4j.appender.A3.MaxFileSize
  • log4j.appender.A3.MaxBackupIndex
  • log4j.appender.file.layout.ConversionPattern

测试日志输出

创建一个测试类,并测试日志输出效果,代码如下:

package com.funtl.hello.spring;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyTest {

    public static final Logger logger = LoggerFactory.getLogger(MyTest.class);

    public static void main(String[] args) {
        logger.info("slf4j for info");
        logger.debug("slf4j for debug");
        logger.error("slf4j for error");
        logger.warn("slf4j for warn");

        String message = "Hello SLF4J";
        logger.info("slf4j message is : {}", message);
    }
}

此时控制台显示为:

2018-06-07 05:15:42,914 INFO [com.funtl.hello.spring.MyTest] - slf4j for info
2018-06-07 05:15:42,915 ERROR [com.funtl.hello.spring.MyTest] - slf4j for error
2018-06-07 05:15:42,915 WARN [com.funtl.hello.spring.MyTest] - slf4j for warn
2018-06-07 05:15:42,916 INFO [com.funtl.hello.spring.MyTest] - slf4j message is : Hello SLF4J

项目根目录下也会多出 logs/log.log 目录及文件

附:占位符说明

打日志的时候使用了 {} 占位符,这样就不会有字符串拼接操作,减少了无用 String 对象的数量,节省了内存。并且,记住,在生产最终日志信息的字符串之前,这个方法会检查一个特定的日志级别是不是打开了,这不仅降低了内存消耗而且预先降低了 CPU 去处理字符串连接命令的时间。

public class MyTest {

    private static final Logger logger = LoggerFactory.getLogger(MyTest.class);

    public static void main(String[] args) {
//        UserService userService = new UserServiceImpl();

        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");
        UserService userService = (UserService) applicationContext.getBean("userService");

        userService.sayHello();

        logger.info("info 级别的日志:打开了数据库....");
        logger.debug("调试级别的日志:list集合中有....");
        logger.warn("警告级别的日志:你不是要记日志吗,怎么没有配置日志等....");
        logger.error("错误级别的日志....");

        String message = "测试";
        String message2 = "测试2";
        // logger.info("message is :" + message); // 加号,创建对象,增加了系统的资源开销,用占位符
        logger.info("message is : {} {}",message,message2);

        // 日志中+拼接浪费资源,字符串中也一样,但是字符串中没有{}占位符,用这样的:
        System.out.println(String.format("message is : %s %s",message,message2));
        System.out.println(message.concat(message2)); // 不推荐这个,和+号拼接差不多
    }
}

日志框架发展史

早年,你工作的时候,在日志里使用了log4j框架来输出,于是你代码是这么写的

import org.apache.log4j.Logger;
\\省略
Logger logger = Logger.getLogger(Test.class);
logger.trace("trace");
\\省略

但是,岁月流逝,sun公司对于log4j的出现内心隐隐表示嫉妒。于是在jdk1.4版本后,增加了一个包为java.util.logging,简称为jul,用以对抗log4j。于是,你的领导要你把日志框架改为jul,这时候你只能一行行的将log4j的api改为jul的api,如下所示

import java.util.logging.Logger;
\\省略
Logger loggger = Logger.getLogger(Test.class.getName()); 
logger.finest("finest");
\\省略

可以看出,api完全是不同的。那有没有办法,将这些api抽象出接口,这样以后调用的时候,就调用这些接口就好了呢?

这个时候jcl(Jakarta Commons Logging)出现了,说jcl可能大家有点陌生,讲commons-logging-xx.jar组件,大家总有印象吧。JCL 只提供 log 接口,具体的实现则在运行时动态寻找。这样一来组件开发者只需要针对 JCL 接口开发,而调用组件的应用程序则可以在运行时搭配自己喜好的日志实践工具。JCL可以实现的集成方案如下图所示

o_log1.png

jcl默认的配置:如果能找到Log4j 则默认使用log4j 实现,如果没有则使用jul(jdk自带的) 实现,再没有则使用jcl内部提供的SimpleLog 实现。

于是,你在代码里变成这么写了

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
\\省略
Log log =LogFactory.getLog(Test.class);
log.trace('trace');
\\省略

至于这个Log具体的实现类,JCL会在ClassLoader中进行查找。这么做,有三个缺点,缺点一是效率较低,二是容易引发混乱,三是在使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露。

于是log4j的作者觉得jcl不好用,自己又写了一个新的接口api,那么就是slf4j。关于slf4j的集成图如下所示

o_log4.png

如图所示,应用调了sl4j-api,即日志门面接口。日志门面接口本身通常并没有实际的日志输出能力,它底层还是需要去调用具体的日志框架API的,也就是实际上它需要跟具体的日志框架结合使用。由于具体日志框架比较多,而且互相也大都不兼容,日志门面接口要想实现与任意日志框架结合可能需要对应的桥接器,上图红框中的组件即是对应的各种桥接器!

我们在代码中需要写日志,变成下面这么写

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//省略
Logger logger = LoggerFactory.getLogger(Test.class);
// 省略
logger.info("info");

在代码中,并不会出现具体日志框架的api。程序根据classpath中的桥接器类型,和日志框架类型,判断出logger.info应该以什么框架输出!注意了,如果classpath中不小心引了两个桥接器,那会直接报错的!

因此,在阿里的开发手册上才有这么一条

强制:应用中不可直接使用日志系统(log4j、logback)中的 API ,而应依赖使用日志框架 SLF4J 中的 API 。使用门面模式的日志框架,有利于维护和各个类的日志处理方式的统一。

ok,至此,基础知识完毕,下面是实战!

项目实战

案例一

一个项目,一个模块用log4j,另一个模块用slf4j+log4j2,如何统一输出?

其实在某些中小型公司,这种情况很常见。我曾经见过某公司的项目,因为研发不懂底层的日志原理,日志文件里头既有log4j.properties,又有log4j2.xml,各种API混用,惨不忍睹!

还有人用着jul的API,然后拿着log4j.properties,跑来问我,为什么配置不生效!简直是一言难尽!

OK,回到我们的问题,如何统一输出!OK,这里就要用上slf4j的适配器,slf4j提供了各种各样的适配器,用来将某种日志框架委托给slf4j。其最明显的集成工作方式有如下:

o_log12.png

进行选择填空,将我们的案例里的条件填入,显然应该选log4j-over-slf4j适配器,就变成下面这张图

o_log13.png

就可以实现日志统一为log4j2来输出!

ps:根据适配器工作原理的不同,被适配的日志框架并不是一定要删除!以上图为例,log4j这个日志框架删不删都可以,你只要能保证log4j的加载顺序在log4j-over-slf4j后即可。因为log4j-over-slf4j这个适配器的工作原理是,内部提供了和log4j一模一样的api接口,因此你在程序中调用log4j的api的时候,你必须想办法让其走适配器的api。如果你删了log4j这个框架,那你程序里肯定是走log4j-over-slf4j这个组件里的api。如果不删log4j,只要保证其在classpth里的顺序比log4j前即可!

案例二

如何让spring以log4j2的形式输出?

spring默认使用的是jcl输出日志,由于你此时并没有引入Log4j的日志框架,jcl会以jul做为日志框架。此时集成图如下

o_log5.png

而你的应用中,采用了slf4j+log4j-core,即log4j2进行日志记录,那么此时集成图如下

o_log6.png

那我们现在需要让spring以log4j2的形式输出?怎么办?

OK,第一种方案,走jcl-over-slf4j适配器,此时集成图就变成下面这样了

o_log8.png

在这种方案下,spring框架中遇到日志输出的语句,就会如上图红线流程一样,最终以log4J2的形式输出!

OK,有第二种方案么?

有,走jul-to-slf4j适配器,此时集成图如下

o_log14.png

ps:这种情况下,记得在代码中执行

SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();

这样jul-to-slf4j适配器才能正常工作。

天啦噜!要死循环

假设,我们在应用中调用了sl4j-api,但是呢,你引了四个jar包,slf4j-api-xx.jar,slf4j-log4j12-xx.jar,log4j-xx.jar,log4j-over-slf4j-xx.jar,于是你就会出现如下尴尬的场面

o_log3.png

如上图所示,在这种情况下,你调用了slf4j-api,就会陷入死循环中!slf4j-api去调了slf4j-log4j12,slf4j-log4j12又去调用了log4j,log4j去调用了log4j-over-slf4j。最终,log4j-over-slf4j又调了slf4j-api,陷入死循环!

异常

异常

异常概念

异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是:

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

注意: 在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.

异常体系

异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception

Throwable体系:

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

异常分类

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

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

  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
  • 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)

异常的产生过程解析

先运行下面的程序,程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生的过程。

工具类

public class ArrayTools {
    // 对给定的数组通过给定的角标获取元素。
    public static int getElement(int[] arr, int index) {
        int element = arr[index];
        return element;
    }
}

测试类

public class ExceptionDemo {
    public static void main(String[] args) {
        int[] arr = { 34, 12, 67 };
        intnum = ArrayTools.getElement(arr, 4)
        System.out.println("num=" + num);
        System.out.println("over");
    }
}

上述程序执行过程图解:

异常的产生和处理

异常的产生

throw关键字的作用

在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

throw关键字的使用格式

throw new 异常类名(参数);

例如:

throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

案例演示

/
public class Test {
    public static void main(String[] args) {
        /*
            throw关键字:
                作用:throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。
                格式: throw 异常对象;
         */
        int[] arr = {10,20,30,40};
        method(arr,4);
    }

    /**
     * 查找指定索引位置的元素
     * @param arr
     * @param index
     */
    public static void method(int[] arr,int index){
        if (index < 0 || index > arr.length-1){
            // 索引不存在-->产生一个异常
            throw new ArrayIndexOutOfBoundsException(index+"");
        }else{
            int num = arr[index];
            System.out.println(num);
        }
    }
}

声明处理异常

声明处理异常的概述

声明处理异常:使用throws关键字将问题标识出来, 表示当前方法不处理异常,而是提醒给调用者, 让调用者来处理....最终会到虚拟机,虚拟机直接结束程序,打印异常信息。

声明处理异常格式

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{  // 可以抛出一个,也可以多个
}	

案例演示


public class Test {
    // 使用throws关键字将异常标识出来, 表示当前方法不处理异常,而是提醒调用者来处理
    public static void main(String[] args) throws ParseException {
        /*
            处理异常的目的: 为了让程序可以继续往下执行
            声明处理异常:
                概述:使用throws关键字将问题标识出来, 表示当前方法不处理异常,而是提醒给调用者,
                    让调用者来处理....最终会到虚拟机,虚拟机直接结束程序,打印异常信息。

                格式:
                    修饰符 返回值类型 方法名(形参列表) throws 异常类名1,异常类名2…{  // 可以抛出一个,也可以多个

                    }
                特点: 声明处理异常,处理完后,如果程序运行的时候出现异常,程序还是无法继续往下执行
                使用场景:  声明处理异常一般处理运行的时候不会出现异常的编译异常

         */
        //method1();

        // 举例:声明处理异常一般处理运行的时候不会出现异常的编译异常
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = sdf.parse("1999-10-10");
        System.out.println(date);

        // 举例:一般用来处理编译异常,让程序通过编译,但程序运行的时候出现异常,程序还是无法继续往下执行
        /*SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = sdf.parse("1999年10月10日");
        System.out.println(date);*/

        System.out.println("结束");

    }


    // 一次声明处理一个异常
    // 使用throws关键字将异常标识出来, 表示当前方法不处理异常,而是提醒调用者来处理
    public static void method1() throws ParseException{
        // 产生一个异常对象
        throw new ParseException("解析异常",1);
    }

    // 一次声明处理多个异常
    public static void method2(int num) throws ParseException,FileNotFoundException{
        // 产生一个异常对象
        if (num == 1){
            throw new ParseException("解析异常",1);
        }else{
            throw new FileNotFoundException("文件找不到异常");
        }
    }

}

捕获处理异常try…catch

捕获处理异常的概述

  • 捕获处理异常:对异常进行捕获处理 , 处理完后程序可以正常向下执行。

捕获处理异常格式

try{
     编写可能会出现异常的代码
}catch(异常类型  e){
     处理异常的代码
     //记录日志/打印异常信息
}
执行步骤:
	1.首先执行try中的代码,如果try中的代码出现了异常,那么就直接执行catch()里面的代码,执行完后,程序继续往下执行
    2.如果try中的代码没有出现异常,那么就不会执行catch()里面的代码,而是继续往下执行

注意:

  1. try和catch都不能单独使用,必须连用。
  2. try中的代码出现了异常,那么出现异常位置后面的代码就不会再执行了
  3. 捕获处理异常,如果程序出现了异常,程序会继续往下执行

​ 声明处理异常,如果程序出现了异常,程序就不会继续往下执行

演示如下:


public class Test {
    public static void main(String[] args) {
        /*
            捕获处理异常: try...catch..
                概述:对异常进行捕获处理 , 处理完后程序可以正常向下执行。
                格式:
                    try{
                         编写可能会出现异常的代码
                    }catch(异常类型  变量名){
                         处理异常的代码
                         //记录日志/打印异常信息
                    }
                执行步骤:
	                1.首先执行try中的代码,如果try中的代码出现了异常,那么就直接执行catch()里面的代码,执行完后,程序继续往下执行
                    2.如果try中的代码没有出现异常,那么就不会执行catch()里面的代码,而是继续往下执行
         */
        method1();
        System.out.println("======================");
        // 捕获处理运行异常
        try {
            System.out.println(1/1);// 没有出现异常,1
        }catch (ArithmeticException e){
            System.out.println("出现了数学运算异常");
        }
        System.out.println("======================");

        try {
            System.out.println(1/0);// 出现了异常
            System.out.println("try...");
        }catch (ArithmeticException e){
            System.out.println("出现了数学运算异常");
        }
        System.out.println("结束");
    }

    // 捕获处理编译异常
    public static void method1(){
        try{
            throw new ParseException("解析异常",1);
        }catch (ParseException e){
            System.out.println("出现了异常");
        }
        System.out.println("method1方法结束...");
    }
}

获取异常信息

Throwable类中定义了一些查看方法:

  • public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

  • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。

包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

在开发中呢也可以在catch将编译期异常转换成运行期异常处理。


public class Test {
    public static void main(String[] args) {
        /*
            Throwable获取异常信息的方法:
            - public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。
            - public String toString():获取异常的类型和异常描述信息(不用)。
            - public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。
         */
        System.out.println("开始");

        try {
            System.out.println(1/0);// 报异常,产生一个异常对象

        }catch (ArithmeticException e){
            /*System.out.println("出现了异常");
            System.out.println(e.getMessage());
            System.out.println(e);
            System.out.println(e.toString());*/
            e.printStackTrace();
            
        }

        System.out.println("结束");
    }
}

finally 代码块

finally代码块的概述

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

finally代码块的语法格式

try{
    可能会出现异常的代码
        
}catch(异常的类型 变量名){
    处理异常的代码或者打印异常的信息
}finally{
    无论异常是否发生,都会执行这里的代码(正常情况,都会执行finally中的代码,一般用来释放资源)
}

执行步骤:
 1.首先执行try中的代码,如果try中的代码出现了异常,那么就直接执行catch()里面的代码,执行完后会执行finally中的代码,然后程序继续往下执行
 2.如果try中的代码没有出现异常,那么就不会执行catch()里面的代码,但是还是会执行finally中的代码,然后程序继续往下执行

注意:finally不能单独使用。

案例演示


public class Test {
    public static void main(String[] args) {
        /*
            finally 代码块:
                概述:在finally代码块中存放的代码都是一定会被执行的
                格式:
                    try{
                        可能会出现异常的代码

                    }catch(异常的类型 变量名){
                        处理异常的代码或者打印异常的信息
                    }finally{
                        无论异常是否发生,都会执行这里的代码(正常情况,都会执行finally中的代码,一般用来释放资源)
                    }

                执行步骤:
                 1.首先执行try中的代码,如果try中的代码出现了异常,那么就直接执行catch()里面的代码,执行完后会执行finally中的代码,然后程序继续往下执行
                 2.如果try中的代码没有出现异常,那么就不会执行catch()里面的代码,但是还是会执行finally中的代码,然后程序继续往下执行
         */
        System.out.println("开始");
        /*try {
            System.out.println(1/0);
        }catch (ArithmeticException e){
            System.out.println("catch 出现了异常");
            return;
            //System.exit(0);
        }finally {
            System.out.println("finally 无论是否发生异常都会执行");
        }*/

        System.out.println("================================");
        try {
            System.out.println(1/1);// 1
            return;
        }catch (ArithmeticException e){
            System.out.println("catch 出现了异常");
        }finally {
            System.out.println("finally 无论是否发生异常都会执行");
        }
        System.out.println("结束");
    }
}

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

image-20220726134105802

finally经典面试题


public class Test {
    public static void main(String[] args) {
        System.out.println(method1());// 30
        System.out.println(method2());// 20
    }

    public static int method1() {
        int num = 10;
        try {
            System.out.println(1 / 0);
        } catch (ArithmeticException e) {
            num = 20;
            return num;
        } finally {
            num = 30;
            return num;
        }
    }

    public static int method2() {
        int num = 10;
        try {
            System.out.println(1 / 0);
        } catch (ArithmeticException e) {
            num = 20;
            // catch中的return会做2件事:1.先记录要返回的值,然后执行finally中的代码,2.最后把记录的值返回
            return num;// 记录要返回的值:20
        } finally {
            num = 30;
        }
        return num;
    }
}

异常注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。

  • 如果父类的方法抛出了多个异常,子类覆盖(重写)父类方法时,只能抛出相同的异常或者是他的子集。

  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出

  • 声明处理多个异常,可以直接声明这多个异常的父类异常

  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

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

    1. 多个异常分别处理。
    2. 多个异常一次捕获,多次处理。
    3. 多个异常一次捕获一次处理。
  • 当多异常分别处理时,捕获处理,前边的类不能是后边类的父类

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

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

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

代码如下:


class Fu {
    public void show() throws ParseException, FileNotFoundException {
        // ...
    }

    public void run() {

    }
}

class Zi extends Fu {
    // 2.如果父类的方法抛出了多个异常,子类覆盖(重写)父类方法时,只能抛出相同的异常或者是他的子集。
   /* @Override
    public void show() throws ParseException, FileNotFoundException, IOException {

    }*/

   // 3.父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理
    @Override
    public void run() {
        try {
            throw new FileNotFoundException("");
        }catch (FileNotFoundException e){

        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            异常注意事项:
                1.运行时异常被抛出可以不处理。即不捕获也不声明抛出。
                2.如果父类的方法抛出了多个异常,子类覆盖(重写)父类方法时,只能抛出相同的异常或者是他的子集。
                3.父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
                4.声明处理多个异常,可以直接声明这多个异常的父类异常
                5.在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。
                6.多个异常使用捕获又该如何处理呢?
                    1. 多个异常分别处理。
                    2. 多个异常一次捕获,多次处理。
                    3. 多个异常一次捕获一次处理。
                7.当多异常分别处理时,捕获处理,前边的类不能是后边类的父类
         */
    }

    // 1.运行时异常被抛出可以不处理。即不捕获也不声明抛出。
    public static void show1() {
        System.out.println(1 / 0);
    }

    // 4.声明处理多个异常,可以直接声明这多个异常的父类异常
    public static void show2(int num) throws Exception{
        if (num == 1){
            throw new FileNotFoundException("");
        }else{
            throw new ParseException("",1);
        }
    }

    // 多个异常使用捕获又该如何处理呢?
    // 1. 多个异常分别处理。
    public static void show3(int num) {
        if (num == 1){
            try {
                throw new FileNotFoundException("");
            } catch (FileNotFoundException e) {

            }
        }else{
            try {
                throw new ParseException("",1);
            } catch (ParseException e) {

            }
        }
    }
    // 2. 多个异常一次捕获,多次处理。
    public static void show4(int num) {
        try {
            if (num == 1){
                throw new FileNotFoundException("");
            }else{
                throw new ParseException("",1);
            }
        }catch (FileNotFoundException e){

        }catch (ParseException e){

        }
    }
    // 3. 多个异常一次捕获一次处理。
    public static void show5(int num) {
        try {
            if (num == 1){
                throw new FileNotFoundException("");
            }else{
                throw new ParseException("",1);
            }
        }catch (Exception e){

        }
    }
    // 7.当多异常分别处理时,捕获处理,前边的类不能是后边类的父类
    public static void show6(int num) {
        try {
            if (num == 1){
                throw new FileNotFoundException("");
            }else{
                throw new ParseException("",1);
            }
        }catch (Exception e){

        }/*catch (ParseException e){

        }*/
    }
}

自定义异常

自定义异常概述

为什么需要自定义异常类:

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,例如年龄负数问题,考试成绩负数问题.这些异常在JDK中没有定义过,此时我们根据自己业务的异常情况来定义异常类。

什么是自定义异常类:

在开发中根据自己业务的异常情况来定义异常类.

自定义一个业务逻辑异常: RegisterException。一个注册异常类。

异常类如何定义:

  1. 自定义一个编译期异常: 自定义类 并继承于java.lang.Exception
  2. 自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException

// 编译异常
public class MyException1 extends Exception {

    public MyException1() {
    }

    public MyException1(String message) {
        super(message);
    }
}



// 运行异常
public class MyException2 extends RuntimeException {
    public MyException2() {
    }

    public MyException2(String message) {
        super(message);
    }
}

public class Test {
    public static void main(String[] args)  {
        /*
            异常类如何定义:
                1. 自定义一个编译期异常: 自定义类 并继承于java.lang.Exception。
                2. 自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException。

         */
        //throw new MyException1("自定义异常1");

        throw new MyException2("自定义异常2");
    }
}

自定义异常的练习

要求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

首先定义一个注册异常类RegisterException:

// 业务逻辑异常
public class RegisterException extends Exception {
    /**
     * 空参构造
     */
    public RegisterException() {
    }

    /**
     *
     * @param message 表示异常提示
     */
    public RegisterException(String message) {
        super(message);
    }
}

模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。


public class Test {
    public static void main(String[] args) {
        // 需求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。
        // 1.定义一个数组,存储一些已知用户名
        String[] names = {"jack", "rose", "jim", "tom"};

        // 2.用户输入要注册的用户名
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个用户名:");
        String name = sc.next();

        // 3.循环遍历数组
        for (String s : names) {
            // 4.在循环中,判断用户输入的用户名和已知的用户名是否相同
            if (s.equals(name)) {
                // 5.如果相同,就抛出异常对象
                try {
                    throw new RegisterException("亲,该用户名已经被注册。");
                } catch (RegisterException e) {
                    System.out.println(e.getMessage());
                    return;
                }
            }
        }

        // 6.如果不相同,就显示提示信息
        System.out.println("亲,恭喜您注册成功!");
    }
}

反射

类加载器

类的加载

  • 当我们的程序在运行后,第一次使用某个类的时候,会将此类的class文件读取到内存,并将此类的所有信息存储到一个Class对象中

类的加载时机

  1. 创建类的实例。

  2. 类的静态变量,或者为静态变量赋值。

  3. 类的静态方法。

  4. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。

  5. 初始化某个类的子类。

  6. 直接使用java.exe命令来运行某个主类。

    以上六种情况的任何一种,都可以导致JVM将一个类加载到方法区。

public class Test {
    public static void main(String[] args) throws Exception{
        // 类的加载时机
        //  1. 创建类的实例。
        //  Student stu = new Student();

        // 2. 类的静态变量,或者为静态变量赋值。
        // Person.country = "中国";

        // 3. 类的静态方法。
        // Person.method();

        // 4. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
        // Class<?> c = Class.forName("com.itheima.demo1_类的加载.Student");

        //  5. 初始化某个类的子类。
        // Zi zi = new Zi();

        // 6. 直接使用java.exe命令来运行某个主类。
    }
}

类加载器

类加载器:是负责将磁盘上的某个class文件读取到内存并生成Class的对象。

  • Java中有三种类加载器,它们分别用于加载不同种类的class:
    • 启动类加载器(Bootstrap ClassLoader):用于加载系统类库<JAVA_HOME>\bin目录下的class,例如:rt.jar。
    • 扩展类加载器(Extension ClassLoader):用于加载扩展类库<JAVA_HOME>\lib\ext目录下的class。
    • 应用程序类加载器(Application ClassLoader):用于加载我们自定义类的加载器。

public class Test {
    public static void main(String[] args) {
        /*
            类加载器:
                概述:是负责将磁盘上的某个class文件读取到内存并生成Class的对象。
                如何获取类加载器:
                    类的字节码对象.getClassLoader()
         */
        // 获取Test类的类加载器
        ClassLoader c1 = Test.class.getClassLoader();
        System.out.println(c1);// AppClassLoader

        // 获取Student类的类加载器
        ClassLoader c2 = Student.class.getClassLoader();
        System.out.println(c2);// AppClassLoader

        // 获取String类的类加载器
        ClassLoader c3 = String.class.getClassLoader();
        System.out.println(c3);// null
        //API中说明:一些实现可能使用null来表示引导类加载器。 如果此类由引导类加载器加载,则此方法将在此类实现中返回null

        System.out.println("====================委派机制=================");
        System.out.println(c1.getParent());// PlatformClassLoader
        System.out.println(c1.getParent().getParent());// null


    }
}

反射的概述

反射的引入

  • 问题:IDEA中的对象是怎么知道类有哪些属性,哪些方法的呢?
 通过反射技术对象类进行了解剖得到了类的所有成员。

反射的概念

 反射是一种机制,利用该机制可以在程序运行过程中对类进行解剖并操作类中的所有成员(成员变量,成员方法,构造方法)

使用反射操作类成员的前提

要获得该类字节码文件对象,就是Class对象

反射在实际开发中的应用

* 开发IDE(集成开发环境),比如IDEA,Eclipse
* 各种框架的设计和学习 比如Spring,Hibernate,Struct,Mybaits....

Class对象的获取方式

* 方式1: 通过类名.class获得
* 方式2:通过对象名.getClass()方法获得
* 方式3:通过Class类的静态方法获得: static Class forName("类全名")
    * 每一个类的Class对象都只有一个。
  • 示例代码

public class Student {
    private String name;

    public void method1(){

    }
}

public class Test {
    public static void main(String[] args) throws Exception{
          /*
            Class对象的获取:
                通过类名.class获得
                通过对象名.getClass()方法获得
                通过Class类的静态方法获得: static Class forName("类全名")
           */
        // 1.方式一:通过类名.class获得
        Class<Student> c1 = Student.class;
        System.out.println(c1);

        // 2.方式二:通过对象名.getClass()方法获得
        Student stu = new Student();
        Class<? extends Student> c2 = stu.getClass();
        System.out.println(c2);

        // 3.方式三:通过Class类的静态方法获得: static Class forName("类全名")
        Class<?> c3 = Class.forName("com.itheima.demo2_Class对象的获取.Student");
        System.out.println(c3);

        // 问题:一个类只有一个字节码对象(Class对象)
        System.out.println(c1 == c2);// true
        System.out.println(c1 == c3);// true
    }
}

Class类常用方法

String getSimpleName(); 获得类名字符串:类名
String getName();  获得类全名:包名+类名
T newInstance() ;  创建Class对象关联类的对象
  • 示例代码
public class ReflectDemo02 {
    public static void main(String[] args) throws Exception {
        // 获得Class对象
        Class c = Student.class;
        // 获得类名字符串:类名
        System.out.println(c.getSimpleName());
        // 获得类全名:包名+类名
        System.out.println(c.getName());
        // 创建对象
        Student stu = (Student) c.newInstance();
        System.out.println(stu);
    }
}

反射之操作构造方法

Constructor类概述

反射之操作构造方法的目的
    * 获得Constructor对象来创建类的对象。

Constructor类概述
    * 类中的每一个构造方法都是一个Constructor类的对象

通过反射获取类的构造方法

Class类中与Constructor相关的方法 
1. Constructor getConstructor(Class... parameterTypes)
        * 根据参数类型获得对应的Constructor对象。
        * 只能获得public修饰的构造方法
 2. Constructor getDeclaredConstructor(Class... parameterTypes)
        * 根据参数类型获得对应的Constructor对象
    	* 可以是public、protected、(默认)、private修饰符的构造方法。
 3. Constructor[] getConstructors()
        获得类中的所有构造方法对象,只能获得public的
 4. Constructor[] getDeclaredConstructors()
        获得类中的所有构造方法对象
    	可以是public、protected、(默认)、private修饰符的构造方法。

通过反射执行构造方法

Constructor对象常用方法
1. T newInstance(Object... initargs)
 	根据指定的参数创建对象
2. void setAccessible(true)
   设置"暴力反射"——是否取消权限检查,true取消权限检查,false表示不取消

示例代码


public class Student {
    public String name;
    public int age;

    public Student() {
    }

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

    public Student(String name) {
        this.name = name;
    }

    private Student(int age) {
        this.age = age;
    }

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


public class Test {
    public static void main(String[] args) throws Exception{
        /*
            Constructor类概述
               类中的每一个构造方法都是一个Constructor类的对象

            反射之操作构造方法的目的
                获得Constructor对象来创建类的对象。

            使用:
                1.如何通过反射获取一个类中的构造方法:Class类的方法
                     1. Constructor getConstructor(Class... parameterTypes)
                        * 根据参数类型获得对应的Constructor对象。
                        * 只能获得public修饰的构造方法
                     2. Constructor getDeclaredConstructor(Class... parameterTypes)
                            * 根据参数类型获得对应的Constructor对象
                            * 可以是public、protected、(默认)、private修饰符的构造方法。

                     3. Constructor[] getConstructors()
                            获得类中的所有构造方法对象,只能获得public的
                     4. Constructor[] getDeclaredConstructors()
                            获得类中的所有构造方法对象
                            可以是public、protected、(默认)、private修饰符的构造方法。

                2.如何通过反射执行获取的构造方法:Constructor的方法
                    1. T newInstance(Object... initargs)
                        根据指定的参数创建对象
                    2. void setAccessible(true)
                       设置"暴力反射"——是否取消权限检查,true取消权限检查,false表示不取消

         */
        // 获取Student类的Class对象
        Class<Student> c = Student.class;

        // 获取单个构造方法
        // 获取无参数的构造方法
        Constructor<Student> con1 = c.getDeclaredConstructor();
        System.out.println(con1);

        // 获取满参构造方法
        Constructor<Student> con2 = c.getDeclaredConstructor(String.class, int.class);
        System.out.println(con2);

        // 获取私有构造方法
        Constructor<Student> con3 = c.getDeclaredConstructor(int.class);
        System.out.println(con3);

        System.out.println("================================");
        // 获取所有构造方法
        Constructor<?>[] arr1 = c.getDeclaredConstructors();
        for (Constructor<?> con : arr1) {
            System.out.println(con);
        }

        System.out.println("================================");
        // 通过执行con1表示的构造方法来创建Student对象
        Student stu1 = con1.newInstance();
        System.out.println(stu1);// Student{name='null', age=0}

        // 通过执行con2表示的构造方法来创建Student对象
        Student stu2 = con2.newInstance("张三", 18);
        System.out.println(stu2);// Student{name='张三', age=18}

        // 取消con3表示的构造方法的权限检查
        con3.setAccessible(true);

        // 通过执行con3表示的构造方法来创建Student对象
        Student stu3 = con3.newInstance(19);
        System.out.println(stu3);// Student{name='null', age=19}

    }
}

反射之操作成员方法

Method类概述

反射之操作成员方法的目的
    * 操作Method对象来调用成员方法
Method类概述
    * 每一个成员方法都是一个Method类的对象。

通过反射获取类的成员方法

Class类中与Method相关的方法
* Method getMethod(String name,Class...args);
    * 根据方法名和参数类型获得对应的构造方法对象,只能获得public的

* Method getDeclaredMethod(String name,Class...args);
    * 根据方法名和参数类型获得对应的构造方法对象,包括public、protected、(默认)、private的

* Method[] getMethods();
    * 获得类中的所有成员方法对象,返回数组,只能获得public修饰的且包含父类的

* Method[] getDeclaredMethods();
    * 获得类中的所有成员方法对象,返回数组,只获得本类的,包括public、protected、(默认)、private的

通过反射执行成员方法

Method对象常用方法
*  Object invoke(Object obj, Object... args)
    * 调用指定对象obj的该方法
    * args:调用方法时传递的参数
*  void setAccessible(true)
    设置"暴力访问"——是否取消权限检查,true取消权限检查,false表示不取消

示例代码


public class Student {

    public void show1() {
        System.out.println("show1 方法...");
    }

    public void show2(int num) {
        System.out.println("show2 方法...num: " + num);
    }

    private void show3() {
        System.out.println("show3 方法...");
    }

    private void show4(String str) {
        System.out.println("show1 方法...str: " + str);
    }

    public int show5(int num) {
        System.out.println("show5 方法...num: " + num);
        return 100;
    }

}


public class Test {
    public static void main(String[] args) throws Exception {
        /*
            反射之操作成员方法:
                Method类概述
                      每一个成员方法都是一个Method类的对象。
                反射之操作成员方法的目的
                      操作Method对象来调用成员方法
                使用:
                    1.如何通过反射获取类中的成员方法:Class类的方法
                        * Method getMethod(String name,Class... args);
                        * 根据方法名和参数类型获得对应的构造方法对象,只能获得public的

                        * Method getDeclaredMethod(String name,Class...args);
                        * 根据方法名和参数类型获得对应的构造方法对象,包括public、protected、(默认)、private的

                        * Method[] getMethods();
                        * 获得类中的所有成员方法对象,返回数组,只能获得public修饰的且包含父类的

                        * Method[] getDeclaredMethods();
                        * 获得类中的所有成员方法对象,返回数组,只获得本类的,包括public、protected、(默认)、private的

                    2.如何通过反射执行获取类中成员方法: Method类的方法
                        *  Object invoke(Object obj, Object... args)
                            * 参数1:调用方法的对象
                            * 参数2:调用方法时传递的实际参数
                            * 返回值: 执行的方法的返回值

                        *  void setAccessible(true)
                            设置"暴力访问"——是否取消权限检查,true取消权限检查,false表示不取消

         */
        // 获取Student类的Class对象
        Class<Student> c = Student.class;

        // 获取单个方法
        // 通过反射获取show1方法
        Method m1 = c.getDeclaredMethod("show1");
        System.out.println(m1);

        // 通过反射获取show2方法
        Method m2 = c.getDeclaredMethod("show2", int.class);
        System.out.println(m2);

        // 通过反射获取show3方法
        Method m3 = c.getDeclaredMethod("show3");
        System.out.println(m3);

        // 通过反射获取show4方法
        Method m4 = c.getDeclaredMethod("show4", String.class);
        System.out.println(m4);

        // 通过反射获取show5方法
        Method m5 = c.getDeclaredMethod("show5", int.class);
        System.out.println(m5);

        System.out.println("==============================");
        // 获取所有方法
        Method[] arr = c.getDeclaredMethods();
        for (Method m : arr) {
            System.out.println(m);
        }

        System.out.println("==============================");
        // 通过反射创建Student对象
        Student stu = c.newInstance();

        // 通过反射执行m1表示的show1方法
        m1.invoke(stu);

        // 通过反射执行m2表示的show2方法
        m2.invoke(stu,10);

        // 取消m3表示的方法的权限检查
        m3.setAccessible(true);

        // 通过反射执行m3表示的show3方法
        m3.invoke(stu);

        // 取消m4表示的方法的权限检查
        m4.setAccessible(true);

        // 通过反射执行m4表示的show4方法
        m4.invoke(stu,"itheima");

        // 通过反射执行m5表示的show5方法
        Object res = m5.invoke(stu, 20);// int res = stu.show5(20);
        System.out.println(res);// 100

    }
}

invoke方法理解

数据:

/**
* 反射测试类
* @author nbchen
* @date 2019/08/01
*/
public class ReflectClass {
    /**
     * 反射测试方法,接收字符串数组,遍历输出
     * @param arg
     */
    public void reflectMethod(String[] arg) {
        for (String s : arg) {
            System.out.println(s);
        }
    }
}

测试:

/**
* 测试反射中的invoke方法
* @author nbchen
* @date 2019/08/01
*/
public class TestInvoke {
    @Test
    public void test() throws Exception {
        // 获取字节码对象
        Class<ReflectClass> clazz = (Class<ReflectClass>) Class.forName("com.nbchen.invoke.ReflectClass");
        // 获取一个对象
        Constructor<ReflectClass> con = clazz.getConstructor();
        ReflectClass instance = con.newInstance();
        String[] testArr = new String[]{"aa","bb"};
        // 获取Method对象
        Method method = clazz.getMethod("reflectMethod",String[].class);
        // 调用invoke方法
        /**
         * 报错:java.lang.IllegalArgumentException: wrong number of arguments
         */
        // method.invoke(instance,testArr);

        // 解决办法1
        // method.invoke(instance, (Object) testArr);
        // 解决办法2
        method.invoke(instance, new Object[]{testArr});
    }
}

小结:

bee8f81984f88e92e96f1e0116cbb53f.png

invoke方法的参数,

第一个参数是一个Object类型,也就是调用该方法的对象,

第二个参数是一个可变参数类型,这个可变参数类型怎么能传递给一个数组类型呢?一个是多个参数。一个是一个数组参数,显然参数的个数不匹配,怎么解决呢?

解决办法就是将可变参数变成一个参数:

1.将传递进去的s强转为Object类型

2.将s重新包装成一个Object数组

扩展反射操作成员方法案例


public class Test {
    public static void main(String[] args)throws Exception {
        /*
            Method类:
            public String getName() 获取方法名
         */
        // 需求: 访问Student类中的成员
        // 1.获取该类的字节码对象
        Class<Student> c = Student.class;

        // 2.获取所有构造方法
        Constructor<?>[] cons = c.getDeclaredConstructors();

        // 3.遍历所有的构造方法
        Student stu = null;
        for (Constructor<?> con : cons) {
            stu = (Student) con.newInstance();
        }

        // 4.获取所有的成员方法
        Method[] methods = c.getDeclaredMethods();

        // 5.遍历所有的成员方法
        for (Method m : methods) {
            if (m.getName().equals("show1")){
                m.invoke(stu);
            }

            if (m.getName().equals("show2")){
                m.invoke(stu,10);
            }

            if (m.getName().equals("show5")){
                Object res = m.invoke(stu, 20);
                System.out.println(res);
            }
            // ...
        }
    }
}

反射之操作成员变量【自学】

Field类概述

反射之操作成员变量的目的
    * 通过Field对象给对应的成员变量赋值和取值

Field类概述
    * 每一个成员变量都是一个Field类的对象。

通过反射获取类的成员变量

Class类中与Field相关的方法
* Field getField(String name);
    *  根据成员变量名获得对应Field对象,只能获得public修饰
* Field getDeclaredField(String name);
    *  根据成员变量名获得对应Field对象,包括public、protected、(默认)、private的
* Field[] getFields();
    * 获得所有的成员变量对应的Field对象,只能获得public的
* Field[] getDeclaredFields();
    * 获得所有的成员变量对应的Field对象,包括public、protected、(默认)、private的

通过反射访问成员变量

Field对象常用方法
void  set(Object obj, Object value) 
void setInt(Object obj, int i) 	
void setLong(Object obj, long l)
void setBoolean(Object obj, boolean z) 
void setDouble(Object obj, double d) 

Object get(Object obj)  
int	getInt(Object obj) 
long getLong(Object obj) 
boolean getBoolean(Object ob)
double getDouble(Object obj) 

void setAccessible(true);暴力反射,设置为可以直接访问私有类型的属性。
Class getType(); 获取属性的类型,返回Class对象。

setXxx方法都是给对象obj的属性设置使用,针对不同的类型选取不同的方法。

getXxx方法是获取对象obj对应的属性值的,针对不同的类型选取不同的方法。

示例代码


public class Student {
    public String name;
    private int age;
}


public class Test {
    public static void main(String[] args) throws Exception{
        /*
            Field类概述
                * 每一个成员变量都是一个Field类的对象。

            反射之操作成员变量的目的
                * 通过Field对象给对应的成员变量赋值和取值

            使用:
                1.如何通过反射获取类的成员变量: Class类的方法
                    * Field getField(String name);
                        *  根据成员变量名获得对应Field对象,只能获得public修饰
                    * Field getDeclaredField(String name);
                        *  根据成员变量名获得对应Field对象,包括public、protected、(默认)、private的

                    * Field[] getFields();
                        * 获得所有的成员变量对应的Field对象,只能获得public的
                    * Field[] getDeclaredFields();
                        * 获得所有的成员变量对应的Field对象,包括public、protected、(默认)、private的

                2.如何通过反射访问获取的类的成员变量: Field类的方法
                    Class getType(); 获取属性的类型,返回Class对象。
                    void  set(Object obj, Object value)
                    Object get(Object obj)
                    void setAccessible(true);暴力反射,设置为可以直接访问私有类型的属性。

         */
        // 获取Student类的Class对象
        Class<Student> c = Student.class;
        Student stu = c.newInstance();

        // 获取单个的成员变量
        // 通过反射获取name成员变量
        Field f1 = c.getDeclaredField("name");
        System.out.println(f1);

        // 通过反射获取age成员变量
        Field f2 = c.getDeclaredField("age");
        System.out.println(f2);

        System.out.println("===============================");
        // 获取所有成员变量
        Field[] arr = c.getDeclaredFields();
        for (Field field : arr) {
            System.out.println(field);
        }

        System.out.println("===============================");
        // 获取f1表示的name属性的类型
        System.out.println(f1.getType());
        // 获取f2表示的age属性的类型
        System.out.println(f2.getType());

        System.out.println("===============================");
        // 通过反射给f1表示的name属性赋值
        f1.set(stu,"张三" );// 相当于stu.name="张三";

        // 取消f2表示的属性的权限检查
        f2.setAccessible(true);

        // 通过反射给f2表示的age属性赋值
        f2.set(stu,18 );

        System.out.println("===============================");
        // 通过反射获取f1表示的name属性的值
        System.out.println(f1.get(stu));// 张三

        // 通过反射获取f2表示的age属性的值
        System.out.println(f2.get(stu));// 18


    }
}

注解

注解概述

注解概述

  • 注解(annotation),是一种代码级别的说明,和类 接口平级关系.

    • 注解(Annotation)相当于一种标记,在程序中加入注解就等于为程序打上某种标记,以后,javac编译器、开发工具和其他程序可以通过反射来了解你的类及各种元素上有无标记,看你的程序有什么标记,就去干相应的事

    • 我们之前使用过的注解:

      ​ 1).@Override:子类重写方法时——编译时起作用

      ​ 2).@FunctionalInterface:函数式接口——编译时起作用

      ​ 3).@Test:JUnit的测试注解——运行时起作用

注解的作用

  • 生成帮助文档@author和@version

  • 执行编译期的检查 例如:@Override

  • 框架的配置(框架=代码+配置)

    • 具体使用请关注框架课程的内容的学习。

小结

  1. 注解用在“源码中”,作为一个“标记”。给“注解解析器”看的,告诉“注解解析器”怎样编译、运行下面的代码。
  2. 开发中,我们一般都是使用注解

JDK提供的三个基本的注解

@Override:描述方法的重写.

@SuppressWarnings:压制\忽略警告.

@Deprecated:标记过时


@SuppressWarnings("all")
class Fu{
    public void show(){

    }
}
class Zi extends Fu{
    @Override
    public void show(){

    }
}
public class Demo {
    public static void main(String[] args) {
        /*
            JDK提供的三个基本的注解:
                	@Override:描述方法的重写.
                    @SuppressWarnings:压制\忽略警告.
                    @Deprecated:标记过时
         */
        @SuppressWarnings("all")
        int num;
    }

    @Deprecated
    public static void method1(){

    }

    public static void method2(){

    }
}

自定义注解

自定义注解语法

public @interface 注解名{
     属性
}
  • 示例代码
/**
 * 定义了注解
 *
 */
public @interface Annotation01 {

}

注解属性

格式

  • 数据类型 属性名();

属性类型

​ 1.基本类型

​ 2.String

​ 3.Class类型

​ 4.注解类型

​ 5. 枚举类型

​ 6.以上类型的一维数组类型

  • 示例代码
public @interface Annotation01 {
    // 1.基本数据类型(4类8种)
    int a();
    double b();

    // 2.String类型
    String c();

    // 3.Class类型
    Class d();

    // 4.注解类型
    Annotation02 f();
    
    // 5.枚举类型
    Sex e();
    // 6.以上类型的一维数组类型
    int[] g();
    double[] h();
    String[] i();
    Sex[] j();
    Annotation02[] k();
}

使用注解并给注解属性赋值

 使用注解:
        如果一个注解中有属性,那么使用注解的时候一定要给注解属性赋值
        如果一个注解没用属性,那么就不需要给注解属性赋值,直接使用即可
如何给注解属性赋值:
        @注解名(属性名=值,属性名2=值2) 

案例演示


public @interface MyAnnotation1 {
    // 不带属性的注解
}



public @interface MyAnnotation2 {
    // 带属性的注解
    String name();
    int age();
    String[] arr();
}



@MyAnnotation1
@MyAnnotation2(name="张三",age=18,arr={"itheima","itcast"})
public class Test1 {
    @MyAnnotation1
    String str;

    @MyAnnotation1
    @MyAnnotation2(name="张三",age=18,arr={"itheima","itcast"})
    public static void main(String[] args) {
        /*
            注解使用:
                不带属性的注解:@注解名
                带属性的注解: @注解名(属性名=属性值,属性名=属性值,...)
                
            注意:带有属性的注解在使用的时候一定要给属性赋值,并且所有属性都要赋值
         */
        @MyAnnotation1
        @MyAnnotation2(name="张三",age=18,arr={"itheima","itcast"})
        int num = 10;
    }
}


给注解属性赋值的注意事项

  • 一旦注解有属性了,使用注解的时候,属性必须有值
  • 若属性类型是一维数组的时候,当数组的值只有一个的时候可以省略{}
  • 如果注解中只有一个属性,并且属性名为value,那么使用注解给注解属性赋值的时候,注解属性名value可以省略
  • 注解属性可以有默认值 格式:属性类型 属性名() defaul t 默认值;
public @interface MyAnnotation1 {
    int a();
}

public @interface MyAnnotation2 {
    int[] arr();
}

public @interface MyAnnotation3 {
    int value();
}

public @interface MyAnnotation33 {
    String[] value();
}

public @interface MyAnnotation4 {
    int a() default 10;
}

public class Test {
    public static void main(String[] args) {
        /*
            给注解属性赋值的注意事项:
                - 一旦注解有属性了,使用注解的时候,属性必须有值
                - 若属性类型是一维数组的时候,当数组的值只有一个的时候可以省略{}
                - 如果注解中只有一个属性,并且属性名为value,那么使用注解给注解属性赋值的时候,注解属性名value可以省略
                - 注解属性可以有默认值  格式:属性类型 属性名() defaul t 默认值;

         */
    }

    //  注解属性可以有默认值  格式:属性类型 属性名() defaul t 默认值;
    //@MyAnnotation4
    //@MyAnnotation4()
    @MyAnnotation4(a = 100)
    public static void method4(){

    }

    // 若属性类型是一维数组的时候,当数组的值只有一个的时候可以省略{}
    //如果注解中只有一个属性,并且属性名为value,那么使用注解给注解属性赋值的时候,注解属性名value可以省略
    //@MyAnnotation33(value={"itheima","itcast"})
    //@MyAnnotation33(value={"itheima"})
    //@MyAnnotation33(value="itheima")
    @MyAnnotation33("itheima")
    public static void method33(){

    }

    // 如果注解中只有一个属性,并且属性名为value,那么使用注解给注解属性赋值的时候,注解属性名value可以省略
    //@MyAnnotation3(value=10)
    @MyAnnotation3(10)
    public static void method3(){

    }

    // 若属性类型是一维数组的时候,当数组的值只有一个的时候可以省略{}
    // @MyAnnotation2(arr={10,20,30})
    // @MyAnnotation2(arr={10})
    @MyAnnotation2(arr=10)
    public static void method2(){

    }

    // 一旦注解有属性了,使用注解的时候,属性必须有值
    @MyAnnotation1(a = 10)
    public static void method1(){

    }

}

元注解

什么是元注解

​ 定义在注解上的注解

常见的元注解

​ @Target:表示该注解作用在什么上面(位置),默认注解可以在任何位置. 值为:ElementType的枚举值

​ METHOD:方法

​ TYPE:类 接口

​ FIELD:字段

​ CONSTRUCTOR:构造方法声明

​ @Retention:定义该注解保留到那个代码阶段, 值为:RetentionPolicy类型,默认只在源码阶段保留

​ SOURCE:只在源码上保留(默认)

​ CLASS:在源码和字节码上保留

​ RUNTIME:在所有的阶段都保留

.java (源码阶段) ----编译---> .class(字节码阶段) ----加载内存--> 运行(RUNTIME)

案例:


@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation1 {
    // 限制该注解只能在方法上和类上使用
    // 设置注解保留到运行阶段
}


@MyAnnotation1
public class Test {
    //@MyAnnotation1  // 编译报错
    int num;

    @MyAnnotation1
    public static void main(String[] args) {
        //@MyAnnotation1 // 编译报错
        String str;
    }
}

注解解析

java.lang.reflect.AnnotatedElement接口: Class、Method、Field、Constructor等实现了AnnotatedElement

  • T getAnnotation(ClassannotationType):得到指定类型的注解引用。没有返回null。

  • boolean isAnnotationPresent(Class<?extends Annotation> annotationType):判断指定的注解有没有。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation1 {
    String name();

    int age();

}


public class Test {

    @MyAnnotation1(name="张三",age=18)
    public void show1(){
        System.out.println("show1方法执行了....");
    }

    public void show2(){
        System.out.println("show2方法执行了....");
    }

    public static void main(String[] args) throws Exception{
        /*
            java.lang.reflect.AnnotatedElement接口: Class、Method、Field、Constructor等实现了AnnotatedElement
            - T getAnnotation(Class<T> annotationType):得到指定类型的注解引用。没有返回null。
            - boolean isAnnotationPresent(Class<?extends Annotation> annotationType):判断指定的注解有没有。

         */
        // 需求:1.获取show1方法上面的注解对象
        // 1.1 得到Test类的Class对象
        Class<?> c = Class.forName("com.itheima.demo12_注解解析.Test");

        // 1.2 获得show1方法的Method对象
        Method show1M = c.getDeclaredMethod("show1");

        // 1.3 根据Method对象调用getAnnotation()方法得到注解对象
        MyAnnotation1 a1 = show1M.getAnnotation(MyAnnotation1.class);
        System.out.println(a1.name());
        System.out.println(a1.age());

        System.out.println("======================");

        // 2.需求: 判断某个方法上是否有MyAnnotation1注解
        // 判断show1方法上是否有MyAnnotation1注解
        boolean res1 = show1M.isAnnotationPresent(MyAnnotation1.class);
        System.out.println(res1);// true

        // 判断show2方法上是否有MyAnnotation1注解
        Method show2M = c.getDeclaredMethod("show2");
        boolean res2 = show2M.isAnnotationPresent(MyAnnotation1.class);
        System.out.println(res2);// false

    }
}

完成注解的MyTest案例

需求

​ 在一个类(测试类,TestDemo)中有三个方法,其中两个方法上有@MyTest,另一个没有.还有一个主测试类(MainTest)中有一个main方法. 在main方法中,让TestDemo类中含有@MyTest方法执行. 自定义@MyTest, 模拟单元测试.

思路分析

  1. 定义两个类和一个注解

  2. 在MainTest的main()方法里面:

    //1.获得TestDemo字节码对象
    //2.反射获得TestDemo里面的所有的方法
    //3.遍历方法对象的数组. 判断是否有@MyTest(isAnnotationPresent)
    //4.有就执行(method.invoke())

代码实现

  • MyTest.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

}
  • TestDemo.java
public class TestDemo {
    @MyTest
    public void show1(){
        System.out.println("show1方法执行了...");
    }

    @MyTest
    public void show2(){
        System.out.println("show2方法执行了...");
    }

    public void show3(){
        System.out.println("show3方法执行了...");
    }

}

  • MainTest.java
public class MainTest {

    public static void main(String[] args) throws Exception {
        // 让第一个类中含有@MyTest注解的方法执行
        // 1.获取TestDemo类的字节码对象
        Class<TestDemo> clazz = TestDemo.class;

        // 2.使用字节码对象获取该类中所有方法对象
        Method[] methods = clazz.getDeclaredMethods();

        // 3.循环遍历所有方法对象
        for (Method method : methods) {
            // 4.在循环中,判断遍历出来的方法对象是否含有@MyTest注解
            boolean res = method.isAnnotationPresent(MyTest.class);
            if (res) {
                // 5.如果有,就调用该方法执行
                method.invoke(clazz.newInstance());
            }
        }

    }

}

动态代理

代理模式概述

为什么要有“代理”?生活中就有很多代理的例子,例如,我现在需要出国,但是我不愿意自己去办签证、预定机票和酒店(觉得麻烦 ,那么就可以找旅行社去帮我办,这时候旅行社就是代理,而我自己就是被代理了。

代理模式的定义:被代理者没有能力或者不愿意去完成某件事情,那么就需要找个人代替自己去完成这件事,这个人就是代理者, 所以代理模式包含了3个角色: 被代理角色 代理角色 抽象角色(协议)

静态代理:

public interface Happy {// 协议,被代理者需要代理的方法,就定义在这里,然后让代理者和被代理者去实现
    // 被代理者实现: 为了确保和代理者实现的方法一致
    // 代理者实现: 为了增强被代理者的这些方法
    public abstract void happy();
}


public class JinLian implements Happy {

    public void happy(){
        System.out.println("金莲在happy...");
    }

}


public class WangPo implements Happy{
    // 成员变量
    JinLian jl;

    // 构造方法
    public WangPo(JinLian jl) {
        this.jl = jl;
    }

    // 成员方法
    @Override
    public void happy() {
        System.out.println("王婆以做衣服的名义开好房间,并把2人约到房间里...");
        // 金莲happy
        jl.happy();
        System.out.println("王婆打扫战场...");
    }
}


public class XiMen {
    public static void main(String[] args) {
        /*
            案例: 金莲要找西门happy
            代理模式的定义:被代理者没有能力或者不愿意去完成某件事情,那么就需要找个人代替自己去完成这件事,这个人就是代理者,
            所以代理模式包含了3个角色: 被代理角色     代理角色    抽象角色(协议)

         */
        // 不请代理: 金莲直接找西门happy
        // 创建金莲对象
        JinLian jl = new JinLian();
        // happy
        // jl.happy();

        // 请代理: 静态代理,代理类真实存在
        Happy wp = new WangPo(jl);// wp:代理对象   WangPo类: 代理类   Happy接口: 协议  JinLian: 被代理类
        wp.happy();
    }
}

动态代理介绍

  • 概述 : 动态代理就是直接通过反射生成一个代理对象,代理对象所属的类是不需要存在的

  • 动态代理的获取:

    ​ jdk提供一个Proxy类可以直接给实现接口类的对象直接生成代理对象

动态代理相关api介绍

Java.lang.reflect.Proxy类可以直接生成一个代理对象

  • Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)生成一个代理对象
    • 参数1:ClassLoader loader 被代理对象的类加载器
    • 参数2:Class<?>[] interfaces 被代理对象的要实现的接口
    • 参数3:InvocationHandler h (接口)执行处理类
    • 返回值: 代理对象
    • 前2个参数是为了帮助在jvm内部生成被代理对象的代理对象,第3个参数,用来监听代理对象调用方法,帮助我们调用方法
  • InvocationHandler中的Object invoke(Object proxy, Method method, Object[] args)方法:调用代理类的任何方法,此方法都会执行
    • 参数1:代理对象(慎用)
    • 参数2:当前执行的方法
    • 参数3:当前执行的方法运行时传递过来的参数
    • 返回值:当前方法执行的返回值

案例-代理方法无参数

public interface Happy {// 协议,被代理者需要代理的方法,就定义在这里,然后让代理者和被代理者去实现
    // 被代理者实现: 为了确保和代理者实现的方法一致
    // 代理者实现: 为了增强被代理者的这些方法
    public abstract void happy();
}


public class JinLian implements Happy {

    public void happy(){
        System.out.println("金莲在happy...");
    }

}


public class XiMen {
    public static void main(String[] args) {
        /*
            案例: 金莲要找西门happy
            代理模式的定义:被代理者没有能力或者不愿意去完成某件事情,那么就需要找个人代替自己去完成这件事,这个人就是代理者,
            所以代理模式包含了3个角色: 被代理角色     代理角色    抽象角色(协议)

         */
        // 不请代理: 金莲直接找西门happy
        // 创建金莲对象
        JinLian jl = new JinLian();
        // happy
        // jl.happy();

        // 请代理: 静态代理,代理类真实存在
        Happy wp = new WangPo(jl);// wp:代理对象   WangPo类: 代理类   Happy接口: 协议  JinLian: 被代理类
        wp.happy();

        /*
            问题:
                1.金莲不方便直接找西门happy
                2.金莲的happy方法需要增强一下,例如:happy之前需要开房,happy之后需要打扫战场

            静态代理: 代理类是真实存在的,通过代理类产生代理对象
            动态代理: 代理类是不真实存在的,在程序运行中,直接产生代理对象
            前提: 被代理类需要实现接口
            动态代理实现获取代理对象:
                jdk提供一个Proxy类可以直接给实现接口类的对象直接生成代理对象
                Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)生成一个代理对象
                    参数1loader:被代理类的类加载器
                    参数2interfaces: 被代理类所有实现的接口的Class对象
                    参数3InvocationHandler: 执行处理类
                    前2个参数是为了帮助在jvm内部生成被代理类的代理对象,第3个参数用来监听代理对象调用的方法,帮助我们代理对象调用方法
         */
        System.out.println("=========================================");

        // 使用动态代理直接产生金莲的代理对象
        // 动态代理: 代理类是不真实存在的,但代理类是一定实现了被代理类的接口的
        // p:动态代理产生的代理对象 代理类是不真实存在的
        Happy p = (Happy) Proxy.newProxyInstance(JinLian.class.getClassLoader(), JinLian.class.getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 回调方法: 当代理对象调用了方法,就会来执行该invoke方法, 在该方法中就可以增强被代理类的方法
                // 参数1: 生成的代理对象 这里就是p这个代理对象 (慎用)
                // 参数2: 当前代理对象执行的方法 这里method就是happy()方法对象
                // 参数3: 当前代理对象执行的方法,传入的实际参数
                // 返回值:当前代理对象执行的方法的返回值
                // System.out.println("invoke");
                if (method.getName().equals("happy")){
                    System.out.println("王婆以做头发的名义把金莲和西门约到房间...");
                    // 通过反射来调用被代理对象的方法
                    method.invoke(jl);
                    System.out.println("王婆打扫战场...");
                }
                return null;
            }
        });

        // 代理happy
        p.happy();// 无参数
    }
}

JDK8新特性

方法引用

方法引用概述

  • 方法引用使用一对冒号 :: , 方法引用就是用来在一定的情况下,替换Lambda表达式

方法引用基本使用

  • 使用场景:
    • 如果一个Lambda表达式大括号中的代码和另一个方法中的代码一模一样,那么就可以使用方法引用把该方法引过来,从而替换Lambda表达式
    • 如果一个Lambda表达式大括号中的代码就是调用另一方法,那么就可以使用方法引用把该方法引过来,从而替换Lambda表达式

public class Test {

    public static void show(){
        System.out.println("线程执行了");
    }

    public static void main(String[] args) {
        /*
            方法引用:
                概述:方法引用使用一对冒号 :: , 方法引用就是用来在一定的情况下,替换Lambda表达式
                使用场景:
                    1.如果Lambda表达式的大括号中的代码和另一个方法的方法体一模一样,那么就可以使用方法引用把该方法
                       直接引过来,从而替换Lambda表达式
                    2.如果Lambda表达式的大括号中的代码就是调用另一个方法,那么就可以使用方法引用把该方法
                       直接引过来,从而替换Lambda表达式
         */
        // 创建并启动线程
        new Thread(()->{
            System.out.println("线程执行了");
        }).start();

        // 发现上述的Lambda表达式大括号中的内容和Test类的show方法的方法体一模一样,符合方法引用替换Lambda表达式的场景
        new Thread(Test::show).start();

        System.out.println("=========================================");

        new Thread(()->{
            Test.show();
        }).start();

        // 发现上述的Lambda表达式大括号中的内容就是调用Test类的show方法,符合方法引用替换Lambda表达式的场景
        new Thread(Test::show).start();
    }
}

方法引用的分类

构造方法引用

public class Test2 {
    public static void main(String[] args) {
        //创建集合
        ArrayList<String> list = new ArrayList<>();
        list.add("杨紫");
        list.add("迪丽热巴");
        list.add("陈钰琪");

        // 需求: 把集合中的元素转换为Person对象,打印输出
        list.stream().map(s-> new Person(s)).forEach(s-> System.out.println(s));

        System.out.println("======================");
       
        list.stream().map(Person::new).forEach(s-> System.out.println(s));
	
    }
}

静态方法引用

public class Test2 {
    public static void main(String[] args) {
        //创建集合
        ArrayList<String> list = new ArrayList<>();
        list.add("110");
        list.add("111");
        list.add("112");

        // 需求:把集合中的元素转换为int类型,打印输出
        list.stream().map(s-> Integer.parseInt(s)).forEach(s-> System.out.println(s));

        System.out.println("======================");

        list.stream().map(Integer::parseInt).forEach(s-> System.out.println(s));

    }
}

对象成员方法引用

成员方法有参数

public class Test2 {
    public static void main(String[] args) {
        //创建集合
        ArrayList<String> list = new ArrayList<>();
        list.add("杨紫");
        list.add("迪丽热巴");
        list.add("陈钰琪");

        // 需求:把集合中所有元素打印输出
        list.stream().forEach(s-> System.out.println(s));

        System.out.println("=================================");
        
        list.stream().forEach(System.out::println);

    }
}

类的成员方法引用

成员方法没有参数

public class Test2 {
    public static void main(String[] args) {
        //创建集合
        ArrayList<String> list = new ArrayList<>();
        list.add("杨紫");
        list.add("迪丽热巴");
        list.add("陈钰琪");

        // 需求: 把集合中的元素转换为该元素对应的字符长度,打印输出
        list.stream().map(s->s.length()).forEach(System.out::println);

        System.out.println("=================================");
		//会默认的用参数s去调用String类中的length()方法
        list.stream().map(String::length).forEach(System.out::println);

    }
}

小结

总结:使用方法引用的步骤
    1.分析要写的Lambda表达式的大括号中是否就是调用另一个方法
    2.如果是,就可以使用方法引用替换,如果不是,就不能使用方法引用
    3.确定引用的方法类型(构造方法,成员方法,静态方法,类的成员方法)
    4.按照对应的格式去引用:
        构造方法: 类名::new
        成员方法(有参数): 对象名::方法名
        静态方法: 类名::方法名
        类的成员方法\成员方法(无参数):  类名::方法名

Base64

Base64概述

  • Base64是jdk8提出的一个新特性,可以用来进行按照一定规则编码和解码

Base64编码和解码的相关方法

  • 编码的步骤:

    • 获取编码器
    • 调用方法进行编码
  • 解码步骤:

    • 获取解码器
    • 调用方法进行解码
  • Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:

    • 基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。
    • URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。
    • MIME:输出隐射到MIME友好格式。输出每行不超过76字符,并且使用'\r'并跟随'\n'作为分割。编码输出最后没有行分割。
  • 获取编码器和解码器的方法

    static Base64.Decoder getDecoder() 基本型 base64 解码器。
    static Base64.Encoder getEncoder() 基本型 base64 编码器。
    
    static Base64.Decoder getMimeDecoder() Mime型 base64 解码器。
    static Base64.Encoder getMimeEncoder() Mime型 base64 编码器。
    
    static Base64.Decoder getUrlDecoder() Url型 base64 解码器。
    static Base64.Encoder getUrlEncoder() Url型 base64 编码器。
    
  • 编码和解码的方法:

    Encoder编码器:  encodeToString(byte[] bys)编码
    Decoder解码器:  decode(String str) 解码
    

案例演示

  • 基本
public class Test1 {
    public static void main(String[] args) {
        // 使用基本型的编码器和解码器对数据进行编码和解码:
        // 1.获取编码器
        Base64.Encoder encoder = Base64.getEncoder();

        // 2.对字符串进行编码
        String str = "name=中国?password=123456";
        String str1 = encoder.encodeToString(str.getBytes());

        // 3.打印输出编码后的字符串
        System.out.println("编码后的字符串:"+str1);

        // 4.获取解码器
        Base64.Decoder decoder = Base64.getDecoder();

        // 5.对编码后的字符串进行解码
        byte[] bys = decoder.decode(str1);
        String str2 = new String(bys);

        // 6.打印输出解码后的字符串
        System.out.println("解码后的字符串:"+str2);
    }
}
  • URL
public class Test2 {
    public static void main(String[] args) {

        // 使用URL型的编码器和解码器对数据进行编码和解码:
        // 1.获取编码器
        Base64.Encoder encoder = Base64.getUrlEncoder();

        // 2.对字符串进行编码
        String str = "name=中国?password=123456";
        String str1 = encoder.encodeToString(str.getBytes());

        // 3.打印输出编码后的字符串
        System.out.println("编码后的字符串:"+str1);

        // 4.获取解码器
        Base64.Decoder decoder = Base64.getUrlDecoder();

        // 5.对编码后的字符串进行解码
        byte[] bys = decoder.decode(str1);
        String str2 = new String(bys);

        // 6.打印输出解码后的字符串
        System.out.println("解码后的字符串:"+str2);
    }
}

  • MIME
public class Test3 {
    public static void main(String[] args) {
        // 使用MIME型的编码器和解码器对数据进行编码和解码:
        // 1.获取编码器
        Base64.Encoder encoder = Base64.getMimeEncoder();

        // 2.对字符串进行编码
        String str = "";
        for (int i = 0; i < 100; i++) {
            str += i;
        }
        System.out.println("编码前的字符串:"+str);

        String str1 = encoder.encodeToString(str.getBytes());

        // 3.打印输出编码后的字符串
        System.out.println("编码后的字符串:"+str1);

        // 4.获取解码器
        Base64.Decoder decoder = Base64.getMimeDecoder();

        // 5.对编码后的字符串进行解码
        byte[] bys = decoder.decode(str1);
        String str2 = new String(bys);

        // 6.打印输出解码后的字符串
        System.out.println("解码后的字符串:"+str2);
    }
}

StringJoiner使用

image-20220717120851789

LomboK

LomboK介绍和配置

什么是LomboK

​ Lombok是一个Java库,能自动插入编辑器并构建工具,简化Java开发。

​ 官网: https://www.projectlombok.org/

Lombok的作用

​ 通过添加注解的方式,Lombok能以简单的注解形式来简化java代码,提高开发人员的开发效率。

​ 例如开发中经常需要写的javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法,而且需要维护,当属性多时会出现大量的getter/setter方法,这些显得很冗长也没有太多技术含量,一旦修改属性,就容易出现忘记修改对应方法的失误,使代码看起来更简洁些。

Lombok的配置

  • 添加maven依赖
<dependency>
	<groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
	<version>1.18.8</version>
	<scope>provided</scope>
</dependency>
  • 安装插件

    ​ 使用Lombok还需要插件的配合,我使用开发工具为idea. 打开idea的设置,点击Plugins,点击Browse repositories,在弹出的窗口中搜索lombok,然后安装即可

    image-20191121092349714

  • 解决编译时出错问题

    ​ 编译时出错,可能是没有enable注解处理器。Annotation Processors > Enable annotation processing。设置完成之后程序正常运行。

    image-20191121092543928

小结

  1. Lombox: 就是一个工具, 简化java代码开发
  2. Lombok环境
    • 添加坐标
    • 添加插件

Lombok注解

@Data

​ @Data注解在类上,会为类的所有属性自动生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。

@Data
public class User implements Serializable{
    private Integer id;
    private String username;
    private String password;
    private String address;
    private String nickname;
    private String gender;
    private String email;
    private String status;
}

@Getter/@Setter

​ 如果觉得@Data太过残暴不够精细,可以使用@Getter/@Setter注解,此注解在属性上,可以为相应的属性自动生成Getter/Setter方法.

public class User implements Serializable{
    @Setter
    @Getter
    private Integer id;
    private String username;
    private String password;
    private String address;
    private String nickname;
    private String gender;
    private String email;
    private String status;
}

如果想要自己显示的设置set方法(加一些判断逻辑什么的,也是可以的)。

lombok不会再生成响应set方法。

image-20220717133513431

/**
* Page 是分页的模型对象
* @param <T> 是具体的模块的 javaBean 类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Page<T> {
    public static final Integer PAGE_SIZE_DEFAULT = 4; // 每页默认显示条数

    private Integer pageNo; // 当前页码
    @Builder.Default
    private Integer pageSize = PAGE_SIZE_DEFAULT; // 当前页显示数量
    private Integer pageTotal; // 总页码数
    private Integer pageTotalCount; // 总记录数
    private List<T> items; // 当前页数据

    public void setPageNo(Integer pageNo) {
        // 边界值有效检查
        this.pageNo = pageNo < 1 ? 1 : pageNo > pageTotal ? pageTotal : pageNo;
    }
}

看图标,暗的

image-20220717133535344

@ToString

​ 类使用@ToString注解,Lombok会生成一个toString()方法,默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割。 通过exclude属性指定忽略字段不输出,

@ToString(exclude = {"id"}) 
public class User implements Serializable{
    private Integer id;
    private String username;
    private String password;
    private String address;
    private String nickname;
    private String gender;
    private String email;
    private String status;
}

@xxxConstructor

  • @NoArgsConstructor: 无参构造器
@NoArgsConstructor
public class User implements Serializable{
    private Integer id;
    private String username;
    private String password;
    private String address;
    private String nickname;
    private String gender;
    private String email;
    private String status;
}
  • @AllArgsConstructor: 全参构造器
@AllArgsConstructor
public class User implements Serializable{
    private Integer id;
    private String username;
    private String password;
    private String address;
    private String nickname;
    private String gender;
    private String email;
    private String status;
}

这三个注解都是用在类上的,第一个和第三个都很好理解,就是为该类产生无参的构造方法和包含所有参数的构造方法,第二个注解则使用类中所有带有@NonNull注解的或者带有final修饰的成员变量生成对应的构造方法,当然,和前面几个注解一样,成员变量都是非静态的,另外,如果类中含有final修饰的成员变量,是无法使用@NoArgsConstructor注解的。

三个注解都可以指定生成的构造方法的访问权限,同时,第二个注解还可以用@RequiredArgsConstructor(staticName=”methodName”)的形式生成一个指定名称的静态方法,返回一个调用相应的构造方法产生的对象,下面来看一个生动鲜活的例子:

@RequiredArgsConstructor(staticName = "sunsfan")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
public class Shape {
    private int x;
    @NonNull
    private double y;
    @NonNull
    private String name;
}

实际效果相当于:

public class Shape {
    private int x;
    private double y;
    private String name;

    public Shape(){
    }

    protected Shape(int x,double y,String name){
        this.x = x;
        this.y = y;
        this.name = name;
    }

    public Shape(double y,String name){
        this.y = y;
        this.name = name;
    }

    public static Shape sunsfan(double y,String name){
        return new Shape(y,name);
    }
}

@builder

案例:在派生类(子类)中使用@Builder

父类(用@AllArgsConstructor注释)

/**
* 实体类的基类
* @author nbchen
* @date 2019/08/03
*/
@Data
@AllArgsConstructor
public class BaseEntity implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 创建时间
     */
    private Date created;

    /**
     * 修改时间
     */
    private Date updated;
}

子类(提供完整的构造方法,并添加@Builder注解)

/**
* 用户实体
* @author nbchen
* @date 2019/08/03
*/
@Data
public class TbUser extends BaseEntity {

    /**
     * 用户名
      */
    private String username;

    /**
     * 密码,加密存储
     */
    private String password;

    /**
     * 注册手机号
     */
    private String phone;

    /**
     * 注册邮箱
     */
    private String email;

    @Builder
    public TbUser(Long id, Date created, Date updated, String username, String password, String phone, String email) {
        super(id, created, updated);
        this.username = username;
        this.password = password;
        this.phone = phone;
        this.email = email;
    }
}

泛型实体使用Lombok builder

/**
* Page 是分页的模型对象
* @param <T> 是具体的模块的 javaBean 类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Page<T> {
    public static final Integer PAGE_SIZE_DEFAULT = 4; // 每页默认显示条数


    private Integer pageNo; // 当前页码
    @Builder.Default
    private Integer pageSize = PAGE_SIZE_DEFAULT; // 当前页显示数量
    private Integer pageTotal; // 总页码数
    private Integer pageTotalCount; // 总记录数
    private List<T> items; // 当前页数据
}

使用:

Page.<Book>builder()
        .pageNo(pageNo) // 设置当前页码
        .pageSize(pageSize) // 设置每页显示条数
        .pageTotalCount(pageTotalCount) // 设置总记录数
        .pageTotal(pageTotal) // 设置总页码数
        .items(items) // 设置当前页数据
        .build();

@Builder.Default

实体

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
// 图书类
public class Book implements Serializable {
    private int id; // id
    private String name; // 名称
    private BigDecimal price; // 价格
    private String author; // 作者
    private Integer sales; // 销量
    private Integer stock; // 库存
    private String img_path = "static/img/default.jpg"; // 图书默认地址

使用

@Test
public void addBook() {
    Book book = Book.builder().name("陈志杰自传").author("陈志杰").price(new BigDecimal("100.00")).sales(10000).stock(0).img_path(null).build();
    System.out.println("book = " + book);
    bookDao.addBook(book);
}

默认值这个时候不生效,会被null覆盖。

d7c9ea84c1bcc9cc781621a38b90b8d8.png

原因:

209d8af439869ddc651dd935cf0714f0.png

@Builder将忽略类中的默认值,如果想要保留默认值那么添加@Builder.Default注解或者声明为final

声明final并不合适的,添加@Builder.Default是解决办法。

一开始我是手动写判断,笨拙

@Data
@NoArgsConstructor
//@AllArgsConstructor
@Builder
// 图书类
public class Book implements Serializable {
    private int id; // id
    private String name; // 名称
    private BigDecimal price; // 价格
    private String author; // 作者
    private Integer sales; // 销量
    private Integer stock; // 库存
    private String img_path = "static/img/default.jpg"; // 图书默认地址

    public Book(int id, String name, BigDecimal price, String author, Integer sales, Integer stock, String img_path) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.author = author;
        this.sales = sales;
        this.stock = stock;
        // 判断不为空才赋值
        if (StringUtils.isNotBlank(img_path)) {
            this.img_path = img_path;
        }
    }

这样是有效的。

3d8c8fd1af77e14c96efa9c7589fda3a.png

接下来试下@Builder.Default

8b5295c675976ae831660b16a80f14b7.png

怎么用呢?

这个时候,就不能给它值了

@Test
public void addBook() {
    Book book = Book.builder().name("陈志杰自传").author("陈志杰").price(new BigDecimal("100.00")).sales(10000).stock(0).build(); // 不build imgPath
    System.out.println("book = " + book);
    bookDao.addBook(book);
}

插入成功!

@NonNull

这个注解可以用在成员方法或者构造方法的参数前面,会自动产生一个关于此参数的非空检查,如果参数为空,则抛出一个空指针异常,举个例子来看看:

//成员方法参数加上@NonNull注解
public String getName(@NonNull Person p){
    return p.getName();
}

实际效果相当于:

public String getName(@NonNull Person p){
    if(p==null){
        throw new NullPointerException("person");
    }
    return p.getName();
}

用在构造方法的参数上效果类似,就不再举例子了。

@Cleanup

这个注解用在变量前面,可以保证此变量代表的资源会被自动关闭,默认是调用资源的close()方法,如果该资源有其它关闭方法,可使用@Cleanup(“methodName”)来指定要调用的方法,就用输入输出流来举个例子吧:

public static void main(String[] args) throws IOException {
     @Cleanup InputStream in = new FileInputStream(args[0]);
     @Cleanup OutputStream out = new FileOutputStream(args[1]);
     byte[] b = new byte[1024];
     while (true) {
       int r = in.read(b);
       if (r == -1) break;
       out.write(b, 0, r);
     }
 }

实际效果相当于:

public static void main(String[] args) throws IOException {
     InputStream in = new FileInputStream(args[0]);
     try {
       OutputStream out = new FileOutputStream(args[1]);
       try {
         byte[] b = new byte[10000];
         while (true) {
           int r = in.read(b);
           if (r == -1) break;
           out.write(b, 0, r);
         }
       } finally {
         if (out != null) {
           out.close();
         }
       }
     } finally {
       if (in != null) {
         in.close();
       }
    }
}

是不是简化了很多。

@EqualsAndHashCode

这两个注解也比较好理解,就是生成toString,equals和hashcode方法,同时后者还会生成一个canEqual方法,用于判断某个对象是否是当前类的实例,生成方法时只会使用类中的非静态非transient成员变量,这些都比较好理解,就不举例子了。

当然,这两个注解也可以添加限制条件,例如用@ToString(exclude={“param1”,“param2”})来排除param1和param2两个成员变量,或者用@ToString(of={“param1”,“param2”})来指定使用param1和param2两个成员变量,@EqualsAndHashCode注解也有同样的用法。

@Value

@Data注解综合了3,4,5和6里面的@RequiredArgsConstructor注解,其中@RequiredArgsConstructor使用了类中的带有@NonNull注解的或者final修饰的成员变量,它可以使用@Data(staticConstructor=”methodName”)来生成一个静态方法,返回一个调用相应的构造方法产生的对象。这个例子就也省略了吧…

@Value注解和@Data类似,区别在于它会把所有成员变量默认定义为private final修饰,并且不会生成set方法。

@SneakyThrows

这个注解用在方法上,可以将方法中的代码用try-catch语句包裹起来,捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出,可以使用@SneakyThrows(Exception.class)的形式指定抛出哪种异常,很简单的注解,直接看个例子:

public class SneakyThrows implements Runnable {
    @SneakyThrows(UnsupportedEncodingException.class)
    public String utf8ToString(byte[] bytes) {
        return new String(bytes, "UTF-8");
    }

    @SneakyThrows
    public void run() {
        throw new Throwable();
    }
}

实际效果相当于:

public class SneakyThrows implements Runnable {
    @SneakyThrows(UnsupportedEncodingException.class)
    public String utf8ToString(byte[] bytes) {
        try{
            return new String(bytes, "UTF-8");
        }catch(UnsupportedEncodingException uee){
            throw Lombok.sneakyThrow(uee);
        }
    }

    @SneakyThrows
    public void run() {
        try{
            throw new Throwable();
        }catch(Throwable t){
            throw Lombok.sneakyThrow(t);
        }
    }
}

@Synchronized

这个注解用在类方法或者实例方法上,效果和synchronized关键字相同,区别在于锁对象不同,对于类方法和实例方法,synchronized关键字的锁对象分别是类的class对象和this对象,而@Synchronized得锁对象分别是私有静态final对象LOCK和私有final对象LOCK和私有final对象lock,当然,也可以自己指定锁对象,例子也很简单,往下看:

public class Synchronized {
    private final Object readLock = new Object();

    @Synchronized
    public static void hello() {
        System.out.println("world");
    }

    @Synchronized
    public int answerToLife() {
        return 42;
    }

    @Synchronized("readLock")
    public void foo() {
        System.out.println("bar");
    }
}

实际效果相当于:

public class Synchronized {
   private static final Object $LOCK = new Object[0];
   private final Object $lock = new Object[0];
   private final Object readLock = new Object();

   public static void hello() {
     synchronized($LOCK) {
       System.out.println("world");
     }
   }

   public int answerToLife() {
     synchronized($lock) {
       return 42;
     }
   }

   public void foo() {
     synchronized(readLock) {
       System.out.println("bar");
     }
   }
 }

@Log

这个注解用在类上,可以省去从日志工厂生成日志对象这一步,直接进行日志记录,具体注解根据日志工具的不同而不同,同时,可以在注解中使用topic来指定生成log对象时的类名。不同的日志注解总结如下(上面是注解,下面是实际作用):

@CommonsLog
private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);
@JBossLog
private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class);
@Log
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());
@Log4j
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);
@Log4j2
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);
@Slf4j
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
@XSlf4j
private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);

关于lombok的注解先写到这里,当然,还有其他一些注解需要大家自己去摸索,同时lombok一直在扩展,将来肯定会加入更多的注解元素,拭目以待了。

如果你要用slf4j日志:

435b13eaaf1b944973555cc21e96c1f8.png

<!--log-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>
注意导这个包,它会依赖slf4j-api和log4j。这样才是完整的。如果你只导入了slf4j-api,会报错:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder

这样就可以使用@Slf4j了,否则点不出来info的

log.info("用户名或密码,登录失败");

@log;@log4j;@slf4j;....等,好几个,要用哪个就导入哪个的jar包,然后加对应的注解即可。

如果要打印info信息,再控制台,或者输出到日志文件,还要配置

log4j.properties

#log4j.rootLogger 配置的是大于等于当前级别的日志信息的输出
#log4j.rootLogger 用法:(注意appenderName可以是一个或多个)
#log4j.rootLogger = 日志级别,appenderName1,appenderName2,....
#log4j.appender.appenderName1定义的是日志的输出方式,有两种:一种是命令行输出或者叫控制台输出,另一种是文件方式保存
#                            1)控制台输出则应该配置为org.apache.log4j.PatternLayout
#                            2)文本方式保存应该配置为org.apache.log4j.DailyRollingFileAppender
#                            3)也可以自定义 Appender类
#log4j.appender.appenderName1.layout.ConversionPattern 定义的是日志内容格式
#log4j.appender.appenderName1.file 定义了该日志文件的文件名称
#log4j.appender.appenderName1.DatePattern 定义了日志文件重新生成的时间间隔,如果设置到天,则每天重新生成一个新的日志文件。
#                                         旧的日志文件则以新的文件名保存,文件名称 = log4j.appender.appenderName1.file + log4j.appender.appenderName1.DatePattern
#log4j.appender.appenderName1.Encoding 定义了编码格式
log4j.rootLogger = info,stdout,file
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p][%d{yyyy-MM-dd HH:mm:ss}][%C{1}:%L] - %m%n
log4j.appender.file = org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.file=D:/logs/book/info(+).log
log4j.appender.file.DatePattern= '.'yyyy-MM-dd
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yyyy-MM-dd HH:mm:ss}][%C{1}:%L] - %m%n
log4j.appender.file.Encoding=UTF-8

注意,如果配置文件,编码格式后面有空格,会报警告

log4j:WARN Error initializing output writer.

log4j:WARN Unsupported encoding?

如果你报错了:

log4j:WARN No appenders could be found for logger (com.nbchen.mybatis.d1.jdbc.JdbcTest).

因为你少加了log4j.properties配置文件

小结

注解

  • @Data

    • 用在类上面的 , 生成set,get, toString, hashCode,canEqual、toString方法
  • @Getter

  • 用在字段, 生成get方法

  • @Setter

    • 用在字段, 生成set方法
  • @ToString

    • 用在类上面的 生成toString方法
  • @xxxConstructor

    • 用在类上面的 生成构造方法 (只能生成无参和全参的构造方法)

优缺点

优点:

  1. 能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,提高了一定的开发效率
  2. 让代码变得简洁,不用过多的去关注相应的方法
  3. 属性做修改时,也简化了维护为这些属性所生成的getter/setter方法等

缺点:

  1. 不支持多种参数构造器的重载

JDK源码

标签:Java,String,int,狂野,System,001,println,public,out
From: https://www.cnblogs.com/zuoer96/p/17038973.html

相关文章

  • 浏览器访问 JSP 文件时无法进入 JavaScript 代码
    问题描述浏览器访问<head>中有JavaScript代码的JSP文件:<%@pagecontentType="text/html;charset=UTF-8"language="java"%><html><head><title>$Title......
  • JavaScript学习笔记—常量、标志符
    1.常量使用const声明常量,常量只能赋值一次,重复赋值会报错除了常规的常量外(如PI),有一些对象类型的数据也会生命为常量constPI=3.1415926;console.log(PI);//3.14......
  • Java的深拷贝和浅拷贝的区别
    一、拷贝的引入(1)、引用拷贝创建一个指向对象的引用变量的拷贝。Teacherteacher=newTeacher("Taylor",26);Teacherotherteacher=teacher;System.out.println(te......
  • java不同版本jdk切换
    jdk环境搭建首先要有java环境,然后安装两个不同版本的jdk,我这里就使用java8和java15CLASSPATH.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jarJAVA_HOME%JA......
  • java操作hdfs
    packagecagy.hap;importjava.io.FileNotFoundException;importjava.io.IOException;importorg.apache.hadoop.conf.Configuration;importorg.apache.hadoop.fs.Fil......
  • 用Java写一个PDF,Word文件转换工具
    前言前段时间一直使用到word文档转pdf或者pdf转word,寻思着用Java应该是可以实现的,于是花了点时间写了个文件转换工具源码weloe/FileConversion(github.com)主要功能就......
  • 【java基础】创建不可变集合
    创建不可变集合List<Integer>list=List.of(1,2,3,4);//[1,2,3,4]Set<Integer>set=Set.of(1,2,3,4);//[1,2,3,4]Map<Integer,Integer>map=Map.of(1,2,3,4);//{1......
  • 【java基础】如何创建20元素以上的不可变集合?(Map.of()无法创建20个以上)
    背景由于Map.of()(jdk-9出现)创建的不可变集合无法超过20个参数,所以可以使用下面的办法创建Map<Object,Object>map=Map.ofEntries(hm.entrySet().toArray(newMap.Entry......
  • Java String类
    String类一、String类的理解和创建对象结构剖析String对象用于保存字符串,也就是一组字符序列;字符串常量对象是用双引号括起来的字符序列。例如:jack"字符串常量;......
  • Java07 异常
    一、什么是异常实际工作中,遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求、你的程序要打开某个文件,这个文件可能不存在或者文件格式不对......