1 Java入门
1.1 什么是编程
编程是指使用特定的编程语言编写代码,以便让计算机执行特定的任务或解决问题的过程。通过编程,我们可以指示计算机按照一系列定义好的指令和逻辑来执行特定的操作。
编程的过程通常包括以下步骤:
- 理解问题或目标:明确需要解决的问题或实现的目标。
- 设计算法:确定解决问题的步骤和逻辑,将问题分解成更小的子问题。
- 选择合适的编程语言:根据问题的需求和编程语言的特性,选择合适的编程语言。
- 编写代码:使用选定的编程语言编写代码,按照预定的算法和逻辑实现功能。
- 调试和测试:运行代码,检查和修复可能的错误和问题。
- 运行和优化:在保证正确性的前提下,优化代码的性能和效率。
编程可以应用于各个领域,例如软件开发、网站设计、数据分析、人工智能等。通过编程,我们可以将复杂的问题分解成简单的步骤,并利用计算机的处理能力来解决这些问题,实现自动化和提高工作效率。
1.2 编程语言的发展史
第一阶段:二进制语言,只有计算机能读懂。 0101 0101
第二阶段:汇编语言,计算机能看懂,人类也能看懂。 0x15
第三阶段:编程语言,人类读懂,计算机能读懂。 C、C++、Java、PHP等
1.3 Java发展史
Java是一门面向对象的编程语言,由Sun Microsystems公司的詹姆斯·高斯林(James Gosling)等人开发。以下是Java的发展史的一些关键里程碑:
- 1991年:Java的开发始于Sun Microsystems公司的Green计划,最初被称为Oak。它是为了支持嵌入式系统和家用电器而设计的。
- 1995年:Java首次在公众面前亮相。Sun Microsystems公司发布了Java 1.0版本,并宣布Java的口号是"Write Once, Run Anywhere"(一次编写,到处运行)。这一特性是通过Java虚拟机(Java Virtual Machine, JVM)实现的。
- 1996年:Sun Microsystems公司将Java提交给国际标准化组织(ISO)。
- 1998年:Sun Microsystems公司发布了Java 2平台,其中包括Java Development Kit(JDK)和Java虚拟机。
- 2000年:Sun Microsystems公司发布了Java 2平台的企业版本(Java 2 Platform, Enterprise Edition, J2EE),用于开发企业级应用程序。
- 2004年:Java 5.0发布,引入了众多新特性,如泛型、注解和枚举类型。
- 2006年:Sun Microsystems公司将Java开源,成立了开放源代码项目OpenJDK,并将Java SE的实现代码释放给开源社区。
- 2009年:Oracle收购了Sun Microsystems公司,成为Java的维护者和支持者。
- 2011年:Java 7发布,引入了一些新特性,如字符串switch、try-with-resources和钻石运算符。
- 2014年:Java 8发布,最显著的特性是引入了Lambda表达式和函数式接口。
- 2017年:Java 9发布,其中包括模块化系统(Java Platform Module System, JPMS)和JShell(交互式编程工具)等新特性。
- 2018年:Java 10和Java 11相继发布,Java 11是一个长期支持版本(LTS)。
随着时间的推移,Java逐渐成为广泛应用的编程语言,用于开发各种应用程序,包括桌面应用程序、移动应用程序(Android平台使用的编程语言)和大型企业级应用程序。Java继续发展,不断推出新版本和功能,以满足不断变化的软件开发需求。
2 Java的环境配置
2.1 JDK环境配置
想要使用某些软件,必须要给软件配置环境,配置了环境后,在计算机中可以在任意位置去启动想要使用的软件。
JDK环境配置步骤:
- 右击我的电脑,属性
- 按照下图操作
3.在path再配置一下
2.2 二进制
十进制转二进制与二进制转十进制
2.3 计算机的存储单位
最小的单位:1位表示一个1或者1个0,1个字节(byte)=8位
1KB = 1024B
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
2.4 命令提示符
- 查看网络:ping
- 查看计算机相关信息:ipconfig /all
- 切换盘符:盘名:
- 切换目录:cd 目录名称
- 启动软件:输入应用名称
2.5 JDK、JRE、JVM(三大任务)
- JDK: Java Developement Kit Java开发工具包 ,提供给所有Java开发人员使用的工具包,如果要开发Java程序,必须要安装JDK
- JRE:Java Runtime Ele Java运行时环境,运行Java程序需要的内容,将来如果只要运行一个Java程序,只要有JRE就可以了
- JVM:Java Virtual Machine Java虚拟机 : 所有的Java代码都是由Java虚拟机(JVM)帮助我们运行的
JVM三大任务 :
- 加载 :JVM负责把字节码文件(.class)加载到内存中
- 验证 :JVM对字节码文件进行验证,验证代码是否符合规范
- 执行:验证通过后,将代码执行出来
2.6 Javad的跨平台
- 平台:指的是不同的操作系统
- 跨平台:就是指同一个Java程序,可以在不用的操作系统上运行
- 跨屏台实现原理 :通过不同操作系统的JVM,完成跨平台的操作
2.7 Java的HelloWorld(记事本、手动编程)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
System.out.println("第一个Java代码!");
}
}
- public 表示一个修饰符,公共的意思,表示任意的位置都可以访问这个类
- class 表示类的意思,表示这个Java文件是一个普通的类文
- HelloWorld 表示类的名称,用来标识这个类的,类名一般用大写开头
- 大括号 :这个类中代码的范围
- public static void main(String[] args){} :main方法,固定写法,是程序的入口, 所有的Java程序启动,都是从这个main开始
- System.out.println() : 向控制台输出指定的内容,括号里面写什么,就会输出什么
写入汉字的时候编译报错的问题:
格式编译报错,需要修改文件的编码格式:
修改完之后,重新编译
2.8 Java常用命令:java、javac
java:用来编译指定的.java文件
javac:用来运行编译好的类文件
2.9 Java中的注释
注释:对于代码的解释,写给自己与活着的其他人看。
单行注释:注释一行内容,一般解释一行代码,以//开头。
多行注释:注释多行内容,一般做一些说明性的注释,写在/* */的范围中。
3 数据类型&变量
取名规则:遵循标识符的命名规范。
标识符:标识符指的是Java中,类,方法,变量取名的一些规范名称。
规范:标识符有数字、字母、下划线、¥组成,不能以数字开头,不能和关键字同名。
举例:123abc 错误
正确 _abc123,abc_123,$abc
变量名的取名:遵循标识符的命名规范的基础上,一般采用小驼峰式命名,首字母小写,
后面的每个单词的首字母大写,尽量要遵循,望文生义
举例:userName,productName,productId
类名的取名:遵循标识符的命名规范的基础上,采用大驼峰式的命名,首字母大写,
后面的每个单词的首字母大写。
举例:class User{ },class Person{ },class Product{ }
方法名的取名:遵循标识符的命名规范的基础上,一般采用小驼峰式命名,首字母小写,
后面的每个单词的首字母大写,尽量要遵循,望文生义。
举例:void getUserName(){ }
练习:
每个人,取5个变量名 : 图书编号、图书名、图书作者、图书出版社、图书价格
5个类名 : 书籍类 、作者类、用户类、出版社类、借阅类
5个方法名 : 获取作者名、 添加书籍、修改书籍、获取用户信息、删除用户信息
变量名:bookId,bookName,bookAuthor,bookPress,bookPrice
类名:BookName,BookAuthor,UserName,Press,Borrowing
方法名:authorName,addBook,modifyBook,userInformation,deleteUser
3.1 Java关键字
标识符的名称,不能和关键字同名
Java对大小写敏感,关键字大写是允许作为标识符名的
3.2 常量
常量指的是在程序运行过程中,不会改变的量。
分类如下 :
整数常量、 字符串常量、字符常量 、小数常量、布尔常量、空常量。
public class Demo1{
public static void main(String[] args){
//声明变量
byte b1 = 100;
//声明数值不能超过类型的范围
//byte date2 = 200;
System.out.println(b1);
//System.out.println(date2);
short s1 = 200;
System.out.println(s1);
//long 类型
long l = 1000l;
System.out.println(l);
//float类型
float f = 3.23f;
System.out.println(f);
//double 类型
double d = 23.34;
System.out.println(d);
//字符类型
char c = 'A';
System.out.println(c);
//布尔类型
Boolean b = true;
System.out.println(b);
}
}
3.3 基本数据类型,数据类型的存储空间
数据类型 : 用来表示Java语言中使用的数据的方式
分类 : 基本数据类型 和 引用数据类型
基本数据类型:四类八种:
整数类 : byte 、short 、int 、 long
浮点数类 :float 、double
字符类 : char
布尔类 :boolean
Java中的数据类型,首位都表示符号位,所以取值范围都是位数-1之后做计算
浮点数类型转二进制的方式 :以40.375为例
1,先求整数部分 40 的二进制 ==> 0010 1000
2,求小数部分0.375的二进制 ,小数求二进制的方式是, 小数部分 乘 2 ,满1取1,
不满1取0
0.375 * 2 = 0.75 => 0
0.75 * 2 = 1.5 => 1
0.5 * 2 = 1 => 1
0.375的二进制结果为 : 011
40.375 组合后的二进制值为 : 0010 1000 . 011
3,小数点左移,左移到最高位的1后面
0010 1000 . 011 ==> 001.0 1000 011
移动的位数+ 127 ==> 5 + 127 = 132 =>
4,计算最终的二进制值
符号位 + 132的二进制(移动后数值的二进制) + 小数点后的二进制 后补0
0 + 1000 0100 + 0 1000 011 0000 0000 0000 0000
5,测试结果
public static void main(String[] args) {
int i = 0b01000010001000011000000000000000;
System.out.println(Float.intBitsToFloat(i));
}
注意事项 :
整数类型,默认是int类型
- float类型和int类型的字节相同,位数也相同,但是int的精度更高,float类型的划分是 1个符号位、8个指数位、23个尾数位,它的精度是23位,int就是31位,float类型的取值范围更大
- double类型的划分是 1个符号位 11个指数位 52个 尾数位 ,比long类的范围大,精度比long类型低
- 浮点数类型,默认是double类型
3.4 变量
变量 的概念 : 在程序的运行过程中,可以改变的量,本质是 内存中开辟的一块空间
变量的声明:语法格式 : 数据类型 变量名 = 值;
变量声明注意事项 :
- 变量不可以重复定义,也就是变量名不能重复,值可以被改变
- float类型和long类型变量,后面需要加上后缀 ,float 加f或者F ,long类型加L
- byte类型和short类型变量声明的时候,它们的值不能超过它们的范围
- 没有赋值的变量,不能被使用,必须要赋值之后才能被调用
- 变量的使用,不能超过它的作用域
作用域:就是变量生效的范围,也就是它所在的那个大括号的范围
变量的其他声明方式 :
方式1:
数据类型 变量1,变量2,变量3;
变量1 = 值; 变量2 = 值; 变量3 = 值;
方式2:
数据类型 变量1 = 值,变量2= 值,变量3 = 值;
/**
变量的声明及使用
**/
public class DemoVariable02{
public static void main(String[] args){
//声明变量
//声明整数类型
//语法 : 数据类型 变量名 = 值;
byte b1 = 100;
//byte b2 = 200; //声明的数值,不能超过数据类型的范围
System.out.println(b1);
//System.out.println(b2);
short s1 = 200;
System.out.println(s1);
//int类型的声明
//int b1 = 300; //不可以声明同名的变量
b1 = 110;
System.out.println(b1);
int i = 100;
System.out.println(i);
//long类型
long l = 1000L;
System.out.println(l);
//float类型
float f1 = 3.45F;
System.out.println(f1);
//double类型
double d1 = 2.58;
System.out.println(d1);
//字符类型
char ch = 'A';
System.out.println(ch);
//布尔类型,只有两个值,true或者false
boolean b2 = true;
System.out.println(b2);
//声明变量
int number01 ;
number01 = 100;
System.out.println(number01); //想使用变量必须先赋值
{
int number02 = 200;
System.out.println(number02);
}
//超过作用域范围,无法调用
//System.out.println(number02);
//声明多个变量
int x,y,z;
x = 100;
y=200;
z=300;
int x1 = 100, y1 = 200,z1 = 300;
System.out.println(x);
System.out.println(y);
System.out.println(z);
System.out.println(x1);
System.out.println(y1);
System.out.println(z1);
}
}
练习 :
声明两个变量 a,b ,a = 10,b=20 ,输出这两个变量,输出内容是 : a的值是 :xx ,b的值是:xx
然后将a和b的值做交换,再分别输出a和b。
public class DemoVariable03{
public static void main(String[] args){
/*
声明两个变量 a,b ,a = 10,b=20 ,输出这两个变量,
输出内容是 : a的值是 :xx ,b的值是:xx
然后将a和b的值做交换,再分别输出a和b
*/
int a = 10;
int b = 20;
System.out.println("a的值是:" + a);
System.out.println("b的值是:" + b);
//借助一个中间变量
int c;
c = a; //先把a赋值给c
a = b; //再把b赋值给a
b = c; //再把c赋值给b
System.out.println("a的值是:" + a);
System.out.println("b的值是:" + b);
}
}
3.5 类型转换(大类型&小类型、int&char的转换)
大类型和小类型:大小类型的区分即是他们的取值范围的大小
byte、 short、 int 、long、 float、 double
大小数据类型的转换:小类型->大类型 : 隐式的数据转换,一定是安全的
大类型 -> 小类型 : 显示的数据类型转换
语法: 小类型 = (小类型) 大类型值;
public class DemoVariable04{
public static void main(String[] args){
//隐式的数据类型转换,小类型 -> 大类型
byte b = 100;
int i = b;
System.out.println(i);
//小类型向大类型转换的时候,会产生精度丢失问题
float f1 = 2.58F;
double d1 = f1; // 2.5799999237060547
System.out.println(d1);
int num = 1234;
float f2 = num;
System.out.println(f2);
//byte类型或者short类型,如果做运算,会被提升类型为int,结果需要做强制转换
byte b1 = 50;
byte b2 = 50;
int b3 = b1 + b2; //byte + byte => int + int
byte b4 = (byte)(b1 + b2);
System.out.println(b3);
System.out.println(b4);
//显示的数据类型转换, 大 -> 小
int num01 = 100;
byte num02 = (byte)num01;
System.out.println(num02);
//大类型的值,转为小类型,需要强制转换,
//如果超过了小类型的范围,可以转换成功,但是最终的结果会发生改变
int num03 = 200;
byte num04 = (byte)num03;
System.out.println(num04); //-56
double num05 = 2.56;
float f3 = (float)num05;
System.out.println(f3);
int num06 = (int)num05;
System.out.println(num06); //2
//字符
char ch01 = 'a';
System.out.println(ch01 + 1); //98
float f4 = (float)2.56; //通过将double类型 2.56强转为float
float f5 = 2.56f; //声明的时候,直接声明为float类型
}
}
4 运算符
4.1 算术运算符
1 表达式:
指的是用运算符连接起来的的式子,比如:a+b,a>3.
2 一元运算符:
只需要一个数据就可以进行操作的运算符。例如:自增++,自减--,-负号
++ :表示让某个值自身加1
-- :表示让某个值自身减1
++和-- 可以写在变量之前,或者写在变量之后,写法:a++或者++a
++在前面和在后面的区别:
- 如果是自身的运算,结果上是没有区别的。
- 如果是做混合计算,存在区别
a++ ,先将a参与表达式运算,然后再给自身加1
++a,先给自身加1,然后再参与表达式的运算
int a = 10;
int b = a++;
int c = --b;
int d = ++c + b++;
System.out.println( a + "--" + b + "--" + c + "--" + d);
4.2 二元运算符
二元运算符:需要两个数据才可以进行操作的运算符。例如:加法+、赋值=
四则运算 :+ - * /
求余 :%
加法 : +
如果是数字类型使用+,那么就是求和。
如果是char类型使用+,那么会将char转为对应的数值(ascii码值)参与计算。
如果是字符串类型使用+,那么会将后面的内容,拼接到字符串的后面。
减法:- 没有特殊的用法,用来给数值求差值
乘法:* 用来给数值求乘积的
除法:/ ,用来给数值求商,整数参与运算后,会将结果的小数部分省略,保留整数部分
求余:% , 用来求两个数值之间的余数
int a = 10;
int b = a++;
int c = --b;
int d = ++c+b++;
System.out.println(a+"--"+b+"--"+c+"--"+d);
//数字类型使用
System.out.println(a + b);
//char类型使用
System.out.println('a' + b);
//字符串:把后面的值拼接到字符串的后面
System.out.println("100" + 10);
System.out.println(100 / 3); //取整
System.out.println(2.55 / 1.2); //仍是浮点数
练习 :
有一个五位数的数字,12345 ,希望这个数字和 10 做运算,得出这个5位数的 个十百千万位的值
int number = 12345;
int unit = number % 10; // 个位
int ten = (number / 10) % 10; // 十位
int hundred = (number / 100) % 10; // 百位
int thousand = (number / 1000) % 10; // 千位
int tenThousand = (number / 10000) % 10; // 万位
System.out.println("个位:" + unit);
System.out.println("十位:" + ten);
System.out.println("百位:" + hundred);
System.out.println("千位:" + thousand);
System.out.println("万位:" + tenThousand);
4.3 关系运算符
关系运算符也可以称为“比较运算符”,用于用来比较判断两个变量或者常量的大小,结果是true或者false 如:> , >= , < , <= , ==
public class Demo2 {
public static void main(String[] args) {
//比较运算符,变量
int a = 10,b = 20;
System.out.println(a > b);
System.out.println(b > a);
System.out.println(b >= a);
//比较常量
System.out.println(a > 5);
//和多个数值比较
System.out.println(a > 5 && a < 20);
//== 判断数值是否相等
//直接比较基本类型
System.out.println(10 == 10);
System.out.println(a == 10);
System.out.println(a == b);
System.out.println('a' == 97);
}
}
4.4 逻辑运算符
逻辑运算符是把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,以判断程序中的表达式是否成立,判断结果为true或false
&& 逻辑与(短路与):用于判断多个条件是否同时为真,只有当所有操作数都为真时,结果才为真。如果任何操作数为假,则结果为假。当遇到第一个为假的操作数时,后面的操作数将不再进行判断,直接返回结果。
|| 逻辑或(短路或):用于判断多个条件是否至少有一个为真,只要有一个操作数为真,结果就为真。如果所有操作数都为假,则结果为假。当遇到第一个为真的操作数时,后面的操作数将不再进行判断,直接返回结果。
! 逻辑取反:用于对单个操作数进行取反操作,将一个为真的操作数取反为假,将一个为假的操作数取反为真。
& :逻辑与
| :逻辑或
public class Demo3 {
public static void main(String[] args) {
//逻辑运算符
//&& 将多个表达式连接起来,如果每个表达式的结果都为true,最终结果才为true
int a =10,b =20;
System.out.println(a > 5 && b > 10);
System.out.println(a < 5 && b > 10);
//& 将多个表达式连接起来,如果每个表达式的结果都为true,最终结果才为true
System.out.println(a > 5 && b > 10);
System.out.println(a < 5 && b > 10);
//短路效果,&&中,前面的表达式如果是false,后面的表达式就不会执行了
System.out.println (a < 5 && b++ > 10);
System.out.println(b);
System.out.println (a < 5 & b++ > 10);
System.out.println(b);
//|| 逻辑或,如果表达式都为false,结果为false,有一个true,最终结果为true
System.out.println(a < 5 || b > 10);
System.out.println(a < 5 | b > 10);
//|| 短路效果,前面表达式为true,后面表达式就不计算了
System.out.println(a > 5 || b++ < 10);
System.out.println(b); //21
// ! 逻辑取反 ,值是true,取反后为false ,值是false,取反后为true
System.out.println(!(a > 5)); //false
System.out.println(!true); //false
}
}
4.5 为运算符(二进制)
&: 位与运算符 ,连接两个数值进行运算,将数值转成二进制后,按位做运算,如果同时为1,结果为1,否则为0
100&20
0110 0100
0001 0100
0000 0100 --->4
|: 位或运算符,连接两个数值进行运算,将数值转成二进制后,按位做运算,有一位为1,结果为1,否则为0
100|20
0110 0100
0001 0100
0111 0100 --->116
^:位异或运算符,连接两个数值进行运算,将数值转成二进制后,按位做运算,位不同为1,位相同为0
100|20
0110 0100
0001 0100
0111 0000 --->112
~:位取反运算符,只对一个数值进行运算,将数值转成2进制后,对补码进行运算,运算完之后,再将补码变回原码
正数 的原码、反码、补码 都是一样的
~100
原码:0110 0100
取反:1001 1011
减1:1001 1010
符号位不变取反 :1110 0101 ---> -101
~-100
原码:1110 0100
反码:1001 1011
补码(反码+1): 1001 1100
补码取反: 0110 0011 ---> 99
4.6 位移运算符(二进制)
位移运算符是一种用于对二进制数进行位移操作的运算符。它将数字的二进制表示进行位移,并返回结果。
JavaScript中提供了三种位移运算符:
- 左移运算符(<<):将数字的二进制表示向左移动指定的位数,右侧空出的位用0填充。
- 右移运算符(>>):将数字的二进制表示向右移动指定的位数,左侧空出的位用原始数字的符号位填充。
- 无符号右移运算符(>>>):将数字的二进制表示向右移动指定的位数,左侧空出的位用0填充。
- 需要注意的是,位移运算符只能应用于32位的有符号整数。如果进行位移的数超过了32位,则会自动截断为32位。另外,无符号右移运算符(>>>)将忽略数字的符号位,始终用0填充左侧空出的位。
public class Demo4 {
public static void main(String[] args) {
//位运算符(二进制)
System.out.println(100 & 20);
System.out.println(100 | 20);
System.out.println(100 ^ 20);
System.out.println(~100);
System.out.println(~-100); //99
int i = 200;
// 1100 1000 int类型的是32位,前面都是0,符号位0表示正数,1表示负数
//200转为byte类型后,byte只有8位,1100 1000 只被保留8位,第一位就被当做符号位
//1100 1000 除符号位外,做取反 +1
//10110111 +1 = 10111000 ==>-56
System.out.println((byte)i); //-56
//位移运算
//0000 0010 往左移 4位 0010 0000 32
System.out.println(2 << 4); // 2 * 2^4
//右位移运算符
//0010 0000 往右移2位 ==> 0000 1000 8
System.out.println(32 >> 2); // 32 / 2^2
const unsignedRightShift = num >>> 1; // 无符号右移一位,结果为 101
console.log(unsignedRightShift); // 输出: 5
}
}
4.7 赋值运算符
- 加法赋值运算符(+=):将右侧的值与左侧的变量相加,并将结果赋给左侧的变量。
- 减法赋值运算符(-=):将右侧的值从左侧的变量中减去,并将结果赋给左侧的变量。
- 乘法赋值运算符(*=):将右侧的值与左侧的变量相乘,并将结果赋给左侧的变量。
- 除法赋值运算符(/=):将左侧的变量除以右侧的值,并将结果赋给左侧的变量。
int a = 10; 将10 赋值给a这个变量
public class Demo5 {
public static void main(String[] args) {
//赋值运算符
//+= -= *= /= %=
//a += 2; 表示 a = a + 2;
int a = 10;
a += 5; // a = a + 5; =左边的a,表示a这个变量名, =右边的a,表示找上面的a变量带入到这里
System.out.println(a); //15
a -= 5; // a = a - 5;
System.out.println(a); // 10
a *= 5; // a = a * 5
System.out.println(a); // 50
a /= 5; //a = a/5;
System.out.println(a); //10
a %= 3; //a = a%3;
System.out.println(a);//1
}
}
4.8 三元运算符(三目运算符):嵌套使用
语法 : 变量 = 表达式 ? 值1 : 值2;
如果前面的表达式的结果是true,最终的变量的结果就是值1,如果是false,变量的结果就是值2
public class Demo6 {
public static void main(String[] args) {
//三元运算符
//声明一个变量a = 10, 声明一个变量b,如果a > 20 ,那么b 的值就等于a,否则b的值就等于20
int a = 10;
int b;
b = a > 20 ? a : 20;
System.out.println(b);
//价格的计算,如果购买物品价格超过了500,那么结算总价就打9折,否则就按原价结算
int price = 600;
double total = price > 500 ? price * 0.9 : price;
System.out.println(total);
//定义 x = 10, y = 20 ,z = 30 ,使用三元表达式,找出abc中的最大值
int x = 10,y = 20, z = 30;
int max = x > y ? x : y;
max = max > z ? max : z;
System.out.println(max);
max = (x > y ? x : y) > z ? (x > y ? x : y) : z;
System.out.println(max);
max = 0 ;
max = (x > y ? x : max) > z ? max : z;
System.out.println("最大值:" + max);
}
}
4.9 Scanner(键盘录入)
Scanner 是一个类,这个类是由系统写好了,提供给我们使用的
我们在使用的时候,需要去把这个类的包导入进来,然后就可以使用它了
使用步骤 :
1,导包
import java.util.Scanner;
2,创建对象
Scanner sc = new Scanner(System.in);
3,使用对象scanner调用方法,并把输入的值赋值给变量
int a = scanner.nextInt();
package com.iweb.airui369.day02.operator;
import java.util.Scanner;
public class Demo07 {
public static void main(String[] args) {
//创建Scanner类的对象(变量)
//语法 : 类型 变量名 = 值;
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个数字a:");
//使用对象scanner调用方法,并把输入的值赋值给变量
int a = scanner.nextInt();
System.out.println("请输入一个数字b:");
//使用对象scanner调用方法,并把输入的值赋值给变量
int b = scanner.nextInt();
System.out.println("输入的两个数字的和是:" + (a + b));
}
}
5 流程控制
5.1 顺序结构(代码执行顺序)
在没有其他结构干扰的情况下,任何代码,都是从上到下依次执行。
5.2 分支结构
在代码中,有的时候,执行某些代码,需要满足一定的条件,就需要使用到分支结构。
常用的分支语句,就是 if(){} -- else{} 结构
语法1 : 如果表达式的结果是true,那么就执行它的代码块的内容
if(表达式){
代码块;
}
语法2 : 如果表达式结果为true,那么执行代码块1的内容,如果是false,执行代码块2
(这种结构的分支,都可以转为三元表达式)
if(表达式){
代码块1;
}else{
代码块2;
}
语法3: 如果表达式1的结果是true,执行代码块1,表达式2的结果为true,执行代码块2,.....表达式都不为true,则执行代码块4
if(表达式1){
代码块1;
}else if(表达式2){
代码块2;
}else if(表达式3){
代码块3;
}else{
代码块4;
}
public class Demo01 {
public static void main(String[] args) {
//演示 if 语句
//让用户输入自己考试的分数,获取对应的奖励,如果考试90分以上,可以奖励1000块钱
Scanner scanner = new Scanner(System.in);
System.out.println("请输入你的考试成绩:");
//接收输入的成绩
int score = scanner.nextInt();
//if (score > 90){
// System.out.println("考试成绩超过90分,奖励1000块钱!");
//}
//如果成绩超过90分奖励1000块钱,否则就提示没有奖励
//if (score > 90){
// System.out.println("考试成绩超过90分,奖励1000块钱!");
//}else {
// System.out.println("没有任何奖励!");
//}
//String str = score > 90 ? "考试成绩超过90分,奖励1000块钱!" : "没有任何奖励!";
//System.out.println(str);
//输入考试分数,超过90分奖励1000块钱,超过80分奖励500块钱 ,
// 超过70 分 奖励200块钱,其他情况没有奖励
if (score > 90){
System.out.println("考试成绩超过90分,奖励1000块钱!");
}else if (score > 80){
System.out.println("考试成绩超过80分,少于90,奖励500块钱!");
}else if (score > 70){
System.out.println("考试成绩超过70分,少于80,奖励200块钱!");
}else {
System.out.println("没有奖励!");
}
//输入语文和数学的成绩
//如果两门课都超过90分,奖励1000块钱
//如果只有一门课超过 90 分,另一门课超过80分,可以得到500块钱奖励
//其他情况没有奖励
System.out.println("请输入语文成绩:");
int chineseScore = scanner.nextInt();
System.out.println("请输入数学成绩:");
int mathScore = scanner.nextInt();
//语文 > 90
if (chineseScore > 90 ){
if (mathScore > 90 ){
System.out.println("奖励1000块钱");
}else if(mathScore > 80){
System.out.println("奖励500块钱");
}else {
System.out.println("没有奖励!");
}
//语文 80-90
}else if (chineseScore > 80){
if (mathScore > 90 ){
System.out.println("奖励500块钱");
}else {
System.out.println("没有奖励!");
}
}else {
System.out.println("没有奖励!");
}
}
}
5.3 开关语句 Switch
语法 : 判断switch后面的变量的值,如果变量的值等于值1,执行代码块1,等于值2,执行代码块2,等于值3,执行代码块3...
都不满足,执行default后 的默认代码块
switch (变量){
case 值1 :
代码块1;
break;
case 值2 :
代码块2;
break;
case 值3 :
代码块3;
break;
....
default:
默认代码块;
}
public class DemoSwitch {
public static void main(String[] args) {
//接收用户输入的数字(1-5),返回对应可以做的事情
//1,查询所有用户 2,增加用户 3,删除用户 4,修改用户 5,查询单个用户
Scanner scanner = new Scanner(System.in);
System.out.println("请输入数字1-5,选择对应的操作:");
int num = scanner.nextInt();
//通过switch去判断
switch (num){
case 1:
System.out.println("查询所有用户!");
break;
case 2:
System.out.println("增加用户!");
break;
case 3:
System.out.println("删除用户!");
break;
case 4:
System.out.println("修改用户!");
break;
case 5:
System.out.println("查询单个用户!");
break;
default:
System.out.println("你输入的内容不正确!");
}
}
}
使用switch注意的问题 :
- switch后面的括号中,可以用的数据类型 : byte、short、int、char、String,以及对应的包装类,不能使用long或者浮点型
- case后面的break可以省略,但是会发生case穿透现象,一般不建议省略
- case后面,只能是常量,不能是变量,而且case后面的值不可重复
- default可以不用写,但是不满足条件的情况就无法执行
使用switch完成,让用户输入一个数字(1-7),后台输出对应的内容 ,比如,输入1,则输出 ,今天是星期一,输入超过范围的值则返回输入错误
5.4 循环结构
循环其实就是可以将一些重复性的代码内容,整理起来,利用特殊的语法,完成重复性的操作,从而让代码变的更加简单、简洁
循环必要条件 :
1,需要一个循环结束条件
2,需要一个控制条件变化的代码块
5.4.1 while循环
语法 : 如果表达式的结果是一个布尔值,如果为true,就会执行代码块的内容
while(表达式){
代码块;
}
package com.iweb.airui369.day02.loop;
public class Demo01 {
public static void main(String[] args) {
//System.out.println(1);
//System.out.println(2);
//System.out.println(3);
//System.out.println(4);
//System.out.println(5);
//System.out.println(6);
//System.out.println(7);
//System.out.println(8);
//System.out.println(9);
//System.out.println(10);
//使用while循环输出1-10
//1, 循环需要结束条件
//2,循环体代码需要变化
int i = 1;
while ( i <= 10){ //循环结束条件
System.out.println(i);
i++; //循环条件发生改变
}
int b = 1;
while (true){
System.out.println(b);
b++; //循环条件发生改变
if (b >10){ //终止循环的条件
break;
}
}
//使用while循环求 1-100的和
//1+2+3+4+5+....+100
int c = 1; //声明循环的初始条件变量
int sum = 0; //声明求和结果的变量
while (c <= 100){
// c = 1,sum = 1 ; c=2 sum= 1+2 ;c =3 sum = 1+2+3 ;c = 100 sum = 1+2+3+4+..+100
sum = sum + c;
c++;
}
System.out.println("1-100的和是:" + sum);
//求1-100之间的偶数的和
//第一种写法
int x = 1;
int sum1 = 0;
while (x <= 100){
//如果x是偶数,才去累加
if (x % 2 == 0){
sum1 += x;
}
x++;
}
System.out.println("1-100之间的偶数和:" + sum1);
//第二种写法
int y = 0;
int sum2 = 0;
while (y <= 100){
sum2 += y;
y += 2;
}
System.out.println("1-100之间的偶数和:" + sum2);
//第三种写法 continue ,continue表示,跳出当次循环,继续下次循环
int z = 0;
int sum3 = 0;
while (z <= 100){
//如果z是奇数,那么就跳出当次循环,继续下次循环
z++;
if (z % 2 == 1){
continue;
}
sum3 += z;
}
System.out.println("1-100之间的偶数和:" + sum3);
//将刚才的switch语句,改成循环的形式
//用户选择一个功能,执行完后,可以继续选择,直到选择6,退出循环
// 接收用户输入的数字(1-6),返回对应可以做的事情
//1,查询所有用户 2,增加用户 3,删除用户 4,修改用户
// 5,查询单个用户 6,退出功能
//continue
public static void main(String[] args) {
//接收用户输入的数字(1-5),返回对应可以做的事情
//1,查询所有用户 2,增加用户 3,删除用户 4,修改用户 5,查询单个用户
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请输入数字1-6,选择对应的操作:");
System.out.println("1,查询所有用户");
System.out.println("2,增加用户");
System.out.println("3,删除用户");
System.out.println("4,修改用户");
System.out.println("5,查询单个用户");
System.out.println("6,退出系统");
int num = scanner.nextInt();
//通过switch去判断
switch (num){
case 1:
System.out.println("查询所有用户!");
continue;
case 2:
System.out.println("增加用户!");
continue;
case 3:
System.out.println("删除用户!");
continue;
case 4:
System.out.println("修改用户!");
continue;
case 5:
System.out.println("查询单个用户!");
continue;
case 6:
System.out.println("退出系统!");
break;
default:
System.out.println("你输入的内容不正确!");
continue;
}
break;
}
}
}
5.4.2 do- while 循环
语法:跟while循环的用法相似,相比来说,需要先执行一次代码块,然后再去做循环判断,最少会执行一次代码块内容
do{
代码块
}while(循环条件表达式);
//do-while循环的用法
public class Demo02 {
public static void main(String[] args) {
//使用do-while输出1-10
int i = 1;
do{
System.out.println(i);
i++;
}while (i <= 10);
//使用do-while完成1-100之间的偶数的求和
int sum = 0;
int a =0;
do {
sum += a;
a += 2;
}while (a <= 100);
System.out.println(sum);
}
}
5.4.3 for循环
语法 :
for(初始化语句a;判断条件语句b;控制条件语句c){
代码块;
}
执行流程 :
- 先执行初始化语句a
- 执行判断条件b,如果b的结果为true,执行代码块内容如果b的结果为false,循环结束
- 执行控制条件c
- 再执行b,依次循环
public class Demo03 {
public static void main(String[] args) {
//声明for循环 ,从1输出到10
for (int i = 1;i <= 10; i++){
System.out.println(i);
}
//使用for循环来求1-100的和
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
System.out.println(sum);
//求1-100的偶数和
int sum1 = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
sum1 += i;
}
}
System.out.println("1-100的偶数和为:" + sum1);
//输出所有的水仙花数
//水仙花数是一个三位数,特点是 :
//个位的三次方+十位的三次方+百位的三次方 的和 等于这个数字本身
for (int i = 100; i < 1000; i++) {
//个位的三次方+十位的三次方+百位的三次方的和
int ge = i % 10;
int shi = i / 10 % 10;
int bai = i / 100;
if (i == ge*ge*ge + shi*shi*shi + bai*bai*bai){
System.out.println("水仙花数:" + i);
}
}
}
}
for循环的嵌套使用:
public class Demo04 {
public static void main(String[] args) {
/*
******
******
******
******
*/
//System.out.print("*");
//System.out.print("*");
//System.out.print("*");
//System.out.print("*");
//System.out.print("*");
//System.out.println("*");
//for (int i = 0; i < 6; i++) {
// System.out.print("*");
//}
//System.out.println();
//for (int i = 0; i < 6; i++) {
// System.out.print("*");
//}
//System.out.println();
//for (int i = 0; i < 6; i++) {
// System.out.print("*");
//}
for (int i = 0; i < 4; i++) { //外层循环,控制行
for (int j = 0; j < 6; j++) { //内层的循环,控制列
System.out.print("*");
}
System.out.println();
}
//使用for循环嵌套,完成一个乘法口诀表的编写
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i ; j++) {
System.out.print(j + "*" + i + "=" + (i*j)+ " ");
}
System.out.println();
}
}
}
5.4.4 break、continue、return的区别
- break 只用在switch和循环中
- continue一般用在循环中,表示跳出当次循环,继续下次循环
- return用在方法中的,一般用来结束方法
5.4.4.1 Break用法
/*
break在多重循环中的使用
*/
public class BreakDemo {
public static void main(String[] args) {
wc:for (int i = 0; i < 5; i++) {
nc:for (int j = 0; j < 4; j++) {
if (j == 2){
// break; //默认状态下,跳出内部循环
// break wc;
break nc;
}
System.out.print("*");
}
System.out.println();
}
}
}
5.4.4.2 Continue用法
/*
continue的用法
*/
public class ContinueDemo {
public static void main(String[] args) {
//有10个学生,循环录入学生的成绩,录完之后,统计
//80分以上的学生的占比情况
//1,创建需要的变量,输入对象
double count = 0.0;
Scanner scanner = new Scanner(System.in);
//2,写一个循环,循环10次,每次循环录入一个学生成绩
for (int i = 1; i <= 10; i++) {
//3,录入成绩后,可以判断这个学生的成绩是否超过80
System.out.println("请输入" + i +"号学生的成绩:");
int score = scanner.nextInt();
//4,如果超过80,记录到一个变量,不超过 continue
if (score < 80){
continue;
}
count++;
}
double total = count / 10;
//5,计算占比
System.out.println("超过80分的学生占比为:" + total * 100 +"%");
}
}
5.4.4.3 return的用法:用来结束方法的,不是用来结束循环的
public class ReturnDemo {
public static void main(String[] args) {
for (int i = 1; i < 10; i++) {
if (i == 5){
System.out.println("退出");
//break;
//continue;
return;
}
System.out.println(i);
}
System.out.println("运行结束");
}
// 1 2 3 4 退出 运行结束
// 1 2 3 4 退出 6 7 8 9 运行结束
//1 2 3 4 退出
}
5.4.5 增强for循环
语法:一般用来遍历数组或者集合中的内容。
for(数据类型 变量 : 数据集合){
}
5.4.6 引用数据类型:String,数组,类
创建方式 :
数据类型 变量名 = new 数据类型();
比如 :Scanner sc = new Scanner(System.in);
为什么叫引用类型?
因为,引用类型在创建的时候,都是创建在堆空间中,然后将堆空间中的区域的地址赋值给栈中的变量
这种地址赋值方式,就是一种引用的方式,所以,称为引用类型
5.4.6.1 String类型
String类型,是字符串类型,在Java中,用双引号引起来的内容就是字符串,它是一个比较特殊的引用类型
声明的时候,可以像基本类型一样,使用字面量的方式赋值,也可以通过new关键字来声明
public class StringDemo {
public static void main(String[] args) {
//String类型的声明方式
//方式1: 类似于基本类型的赋值方式
//字符串以字面量的方式赋值,值直接从常量池中获取
//从常量池中拿到的内容就是相同的
String s1 = "hello";
//方式2: 和引用类型赋值方式一样
//s2通过new的方式创建,但是输出的内容仍然是字符串
//而不是一个地址值
String s2 = new String("hello");
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2); //false
String s3 = "hello";
System.out.println(s1 == s3);//true
}
}
5.5 Java内存划分初识
Java程序执行的时候,会将内存分为栈空间、堆空间
栈空间特点 :
- 自动分配,不需要程序员申请
- 栈空间存取数据的效率较高
- 栈空间的数据 按照 先进后出的方式管理
- 栈空间存储空间较小,不能存放太多的数据
- JVM将基本类型的数据存放在栈空间
堆空间特点 :
- 需要程序员申请,通过new关键字申请
- 堆中存取效率相对较低
- 堆空间中存放数据是随机分配位置,所以在申请了空间后,会得到一个地址值,指向变量
- 堆空间存放数据的空间比较到,能存放大量的数据
- 引用类型的数据一般都存放在堆空间中
5.6方法
Java是面向对象的语言,对象都会有一些方法,方法其实就是这些类可以做的事情,也称为函数
方法中,一般都会写一些执行了某些功能的代码
将来可以调用这个方法,从而去完成某些想要的功能
方法一般单独声明于类中
语法:
修饰符 返回值类型 方法名 (参数列表){
代码块;
return 返回值;
}
修饰符 : 暂时的写法 public static
返回值类型 :可以是void ,表示没有返回值 ,也可以是数据类型(基本类型和引用类型)
方法名 : 自己取的名字,遵循标识符命名规范
参数列表 :将来调用这个方法时候,是否需要传入值进行运算,如果不需要,可以不写
参数的定义方式 : 数据类型 变量名
代码块 : 将来方法中执行的功能
return :跟返回值类型挂钩
如果有返回值类型,那么就需要return 对应类型的值,
如果没有返回值类型,return可以省略
public class MethodDemo {
//写一个方法,调用之后,可以传入2个整数,并完成两个整数的计算求和后返回
public static int add(int a,int b){
int sum = a + b; //拿到传入的参数,并计算和
return sum; //把求和的结果返回
}
public static void main(String[] args) {
//调用方法,
//也可以将整个方法的表达式参与运算
System.out.println(add(10, 20));
// 可以声明一个变量,接收返回的值
int result = add(10,20);
System.out.println(result);
}
}
//方法的其他用法
public class MethodDemo02 {
public static void main(String[] args) {
//修饰符 返回值 方法名 (参数列表){
// 代码块;
// return;
// }
add();
//调用方法,传入参数的类型不能有错
add("hello","world");
//调用有返回值的方法
//1,要么将方法结果返回给一个变量
//2,要么将返回值结果,拿过来做运算
String s = addString();
System.out.println(addString());
//调用有参数有返回值的方法
String s3 = addString("你好", "世界");
String s1 = "你好";
String s2 = "世界";
String s4 = addString(s1, s2);
System.out.println(s3);
System.out.println(s4);
}
// 没有返回值的方法,一般在方法中,会把要做的事情处理完
//1,没有返回值,没有参数 ,某些固定的功能,不需要改变的时候使用这种
public static void add(){
String s1 = "hello";
String s2 = "world";
System.out.println(s1 + s2);
}
//2,没有返回值,有参数的情况,一般会根据用户传入的内容,执行对应的功能
public static void add(String s1,String s2){
System.out.println(s1 + s2);
}
//3,有返回值,没有参数的情况
public static String addString(){
//只要有返回值,必须对应有return
String s1 = "hello";
String s2 = "world";
//返回字符串的拼接
return s1+s2;
}
//4,有返回值,有参数的情况
public static String addString(String s1,String s2){
return s1 + s2;
}
//形参 :定义在方法体的括号中的参数
// 实参 :调用方法的时候,传入的实际的内容
}
定义一个方法,调用的时候传入两个整数,传入之后,让两个参数的值做交换后打印输出
输出内容 : 传入的参数为 a = ? b = ? , 交换之后的a = ? b = ?
public static void swap(int a , int b){
System.out.println("传入的参数为 a = " + a + " ,b = " + b);
int c = a;
a = b;
b = c;
System.out.println("交换之后的 a = " + a + " ,b = " + b);
}
5.6.1 方法递归
方法递归其实就是方法在自己的方法体中调用自身
递归在调用的时候,可能会产生栈溢出错误,使用的时候要注意控制递归的次数,而且将来要去指定递归结束的条件
//方法递归的使用
public class MethodDemo03 {
public static void main(String[] args) {
//方法递归
//method01();
int sum = sum01(100);
System.out.println(sum);
int sum02 = sum02(100);
System.out.println(sum02);
}
//public static void method01(){
// int i = 1;
// System.out.println(++i);
// method01();
//}
//定义一个方法,传入参数n,可以返回1-n之间数值的和
//普通循环求法
public static int sum01(int n){
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
return sum;
}
//递归的写法 1+2+3+4+5+...+n
public static int sum02(int n){
if (n == 1){
return 1;
}
return n + sum02(n-1);
}
// n = 1 sum = 1
// n = 2 2 + sum02(1) ==> 2 + 1
// n = 3 3 + sum02(2) => 3 + 2 + 1
//..
}
习题:一只青蛙一次可以跳上1级台阶,也可以跳上2级。
求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)
public class MethodDem04 {
//一只青蛙一次可以跳上1级台阶,也可以跳上2级。
// 求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)
//1 1 1
//2 2 1+1 2
//3 3 1+1+1 1+2 2+1
//4 5 1+1+1+1 1+1+2 1+2+1 2+1+1 2+2
//5 8 1+1+1+1+1 1+1+1+2 1+1+2+1 1+2+1+1 2+1+1+1 2+2+1 2+1+2 1+2+2
public static int jumpMethod(int n){
if (n == 1 || n == 2){
return n;
}
return jumpMethod(n-1) + jumpMethod(n-2);
}
public static void main(String[] args) {
int i = jumpMethod(10);
System.out.println(i);
}
}
6 数组
6.1 数组
6.1.1 数组的定义
数组其实也是声明的一个变量,跟普通的变量不同的是,可以存储相同数据类型的一组数据
数组是一个长度固定的数据结构,数组也是一个对象(引用数据类型)
6.1.2 数组的声明和初始化
语法 :
步骤1: 先声明数组
1,数据类型 变量名[];
2,数据类型[] 变量名;
步骤2: 给数组声明空间
数组名 = new 数组数据类型[数组长度];
示例:
int[] array ; 或者 int array[];
array = new int[10];
合并写法 : int[] array = new int[10];
步骤3: 给数组的空间赋值 ,分配空间,通过数组的索引下标找到对应的空间
array[0] = 10;
//数组的声明
public class Demo01 {
public static void main(String[] args) {
//数组声明:
// 写法1
//创建数组,并声明空间,赋值给变量arr
int[] arr = new int[5];
//给数组空间赋值
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
arr[3] = 40;
arr[4] = 50;
//写法2:已知数组的内容。可以简写
int[] arr1 = new int[]{10,20,30,40,50};
//写法3 : 再简写
int[] arr2 = {10,20,30,40,50};
//使用写法1,声明一个String类型的数组,数组中有5个空间,内容随便写
String[] strArray = new String[5];
strArray[0] = "hello";
strArray[1] = "world";
strArray[2] = "java";
strArray[3] = "mysql";
strArray[4] = "linux";
//使用写法3,声明一个char类型的数组,数组有5个char类型的值
char[] charArray = {'a','b','c','d','e'};
}
}
6.1.3 数组的特征(长度、索引)
长度固定,索引下标从0开始 ,下标的值是 从 0 ~ 长度-1
6.1.4 数组的遍历(循环进行遍历)
public class Demo02 {
public static void main(String[] args) {
int[] arr = {10,20,30,40,50};
//直接输出数组,输出的是数组变量的引用地址
System.out.println(arr);
//通过数组下标,将它的值输出
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
System.out.println(arr[4]);
System.out.println("------------------------");
//使用for循环遍历
//循环的次数 : 跟数组的长度一致
for (int i = 0; i < 5; i++) {
System.out.println(arr[i]);
}
System.out.println("------------------------");
//数组中,有一个length属性,可以返回数组的长度
System.out.println(arr.length); //5
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
//增强for循环完成数组的遍历
//for(数据类型 变量 : 数据集合){
//}
System.out.println("------------------------");
for (int i :arr){
System.out.println(i);
}
//快捷键 :itar
for (int i = 0; i < arr.length; i++) {
int i1 = arr[i];
System.out.println(i1);
}
//iter
for (int i : arr) {
System.out.println(i);
}
}
//定义一个方法,调用方法的时候,可以传入一个数组,
// 然后这个方法可以将数组的内容输出
public static void printArray(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
练习:循环录入5个学生的成绩到数组中,录完之后,将5个人的成绩输出,并输出其中的最高分
public class Demo03 {
public static void main(String[] args) {
//int[] arr = {11,22,33,44,55};
通过Demo02调用输出方法
//Demo02.printArray(arr);
//循环录入5个学生的成绩到数组中
//录完之后,将5个人的成绩输出,并输出其中的最高分
//1,数组长度 5
//2,循环录入
//3,找到最高分
//创建长度为5的数组
int[] arr = new int[5];
//创建输入对象scanner
Scanner scanner = new Scanner(System.in);
//定义一个最大值变量
int max = 0;
for (int i = 0; i < arr.length; i++) {
System.out.println("请输入第" + (i+1) + "个学生的成绩:");
//通过数组下标,获取输入的值
arr[i] = scanner.nextInt();
//将每次输入的值和max做比较,保留大的值
max = max > arr[i] ? max : arr[i];
}
System.out.println("五位学生的成绩是:");
Demo02.printArray(arr);
System.out.println("最高分是:" + max);
}
}
6.1.5 数组的内存分配
1,int[] arr = new int[5] : 表示在内存的堆区申请一个空间,这个空间有5个位置,每个位置的默认值是0,然后将这个区域在内存中的地址,指向变量 arr
2,arr[0] = 10 ; 表示通过 arr对应的地址引用,找到内存中数组的位置空间,通过下标0,找到这个空间中对应下标的值,然后将10 替换掉原有的默认值
6.2 二维数组的创建和遍历
二维数组指的是,在数组中,继续存放数组的一种形式
public class Demo05 {
public static void main(String[] args) {
//创建二维数组
//创建格式 :
//格式1:
//数据类型[][] 数组名 = new 数据类型[m][n]
//格式2:
// 数据类型[][] 数组名 = {{...},{...},{...}};
//
// 有个5个班级,各有5个学生数学课成绩
//将所有班级,所有学生的数学成绩录入,
//录入后,求5个班各自的总成绩
Scanner scanner = new Scanner(System.in);
//定义数组
int[][] array = new int[5][5];
//使用嵌套for循环完成成绩的录入
//i 班级 j 学生
for (int i = 0; i < 5; i++) {
System.out.println("第" + (i+1) + "个班:");
for (int j = 0; j < 5; j++) {
System.out.println("第" + (j+1) + "个学生的成绩:");
array[i][j] = scanner.nextInt();
}
}
//统计成绩:
for (int i = 0; i < array.length; i++) {
int total = 0; //记录总成绩
for (int j = 0; j < array[i].length; j++) {
//拿到每个班的学生的成绩做累加
total += array[i][j];
}
System.out.println("第" + (i+1) + "个班级的总成绩是:" + total);
}
}
}
6.3 基本算法了解
6.3.1 冒泡排序
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成
/*
冒泡排序
*/
public class BubbleSort {
public static void main(String[] args) {
//声明一个数组,
//排序是为了将数组中的数据按照顺序从小到大排列
int[] arr = {25,36,11,5,2};
System.out.println("排序前");
printArray(arr);
/* //第一次排序
for (int i = 0; i < arr.length - 1; i++) {
//将数组的值做两两比较
if (arr[i] > arr[i+1]){
//交换
int temp= arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
System.out.println("第一次排序完:");
printArray(arr);
//第二次排序 25 11 5 2 36
for (int i = 0; i < arr.length-1-1 ; i++) {
//将数组的值做两两比较
if (arr[i] > arr[i+1]){
//交换
int temp= arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
System.out.println("第二次排序完:");
printArray(arr);
//第三次排序
for (int i = 0; i < arr.length-1-1-1 ; i++) {
//将数组的值做两两比较
if (arr[i] > arr[i+1]){
//交换
int temp= arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
System.out.println("第三次排序完:");
printArray(arr);
//第四次排序
for (int i = 0; i < arr.length-1-1-1-1 ; i++) {
//将数组的值做两两比较
if (arr[i] > arr[i+1]){
//交换
int temp= arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
System.out.println("第四次排序完:");
printArray(arr);
for (int i = 0; i < arr.length -1 ; i++) { //外层循环控制总次数
for (int j = 0; j < arr.length-i-1 ; j++) {
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}*/
bubbleSort(arr);
System.out.println("排序完:");
printArray(arr);
}
//定义一个冒泡排序的方法:
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length -1 ; i++) { //外层循环控制总次数
for (int j = 0; j < arr.length-i-1 ; j++) {
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
public static void printArray(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
6.3.2 选择排序
n条数据,经过n-1趟排序,得到有序结果
1.开始排序之前,划分一个有序区域,和无序区域
2.第一次排序开始的时候, 从无序区一个个去寻找最小值,每次找到小的值就做标记,所有数据找完之后,找到了无序区最小的值,把它移动最前面,作为有序区
3.继续在无序区找小的值,每一轮都找到一个无序区的最小值,把这个值,依次往有序区的后面排放(其实是将最小值的位置,和有序区的后一个位置的值做交换)
4.重复以上第3步
//选择排序
public class SelectSort {
public static void main(String[] args) {
int[] arr = {33,12,4,56,23,44,74,3};
System.out.println("排序前:");
printArray(arr);
selectSort(arr);
System.out.println("排序完:");
printArray(arr);
}
//输出方法
public static void printArray(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
//写一个选择排序方法
public static void selectSort(int[] arr){
//存在一个有序区和无序区
//排序开始之前,有序区没有内容
//第一次排序的时候,将第一个数值标记为有序的第一个值
for (int i = 0; i < arr.length - 1; i++) {
//指定一个最小数值的索引
int minIndex = i;
//从有序区的后面的第一个(i+1)开始遍历
for (int j = i+1; j < arr.length ; j++) {
//找到最小的值
if (arr[j] < arr[minIndex]){
//将最小值的下标做交换
minIndex = j;
}
}
//循环之后,做数据交换
if (i != minIndex){
int temp = arr[i] ;
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}
6.3.3 插入排序
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
public class InsertSort {
public static void main(String[] args) {
int[] arr = {33,12,4,56,23,44,74,3};
System.out.println("排序前:");
printArray(arr);
//排序
insertSort(arr);
System.out.println("排序完:");
printArray(arr);
}
//输出方法
public static void printArray(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
//插入排序方法
public static void insertSort(int[] arr){
//第一次循环从第二个元素开始,默认第一个元素是有序的
for (int i = 1; i < arr.length; i++) {
//指定一个插入数
int insertNum = arr[i];
//指定一个已经排序好的元素的个数
int j = i - 1;
//循环判断
//如果插入的值,小于前面的有序区的最后一个值(arr[j]),
//把有序区的最后一个值往后移动,
//然后再和最后一个值前面的值(arr[j-1])作比较,所以,
//循环的时候,需要做j-- 操作
while ( j >= 0 && arr[j] > insertNum){
arr[j+1] = arr[j];
j--;
}
arr[j+1] = insertNum;
}
}
}
6.3.4 快速排序
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
-
从数列中挑出一个元素,称为 “基准”(pivot);
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
public class QuickSort {
//写一个快速排序的方法
public static void quickSort(int[] arr,int left , int right){
//提前声明需要使用的变量
int i,j,temp,t;
//递归结束条件
if (left > right){
return;
}
//开始排序
i = left;
j = right;
temp = arr[left]; //中间值
//循环判断
while (i < j){
//如果j下标的值,比中间值大,j做--操作
while (temp <= arr[j] && i < j){
j--;
}
//如果i下标的值,比中间值小,i做++操作
while (temp >= arr[i] && i < j){
i++;
}
//如果上面两个条件不满足,i 和 j 交换
if(i < j){
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后需要将中间值和 j的值做交换
arr[left] = arr[j];
arr[j] = temp;
//递归调用左边
quickSort(arr,left,j-1);
//递归调用右边
quickSort(arr,j+1,right);
}
//输出方法
public static void printArray(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = {10,31,43,2,34,12,4};
System.out.println("排序前");
printArray(arr);
//调用排序方法
quickSort(arr,0,arr.length-1);
System.out.println("排序后");
printArray(arr);
}
}
测试排序时间:
分别插入10万条、100万条数据再做测试
/*
测试排序时间
*/
public class SortTime {
public static void main(String[] args) {
//循环往数组中,存放1万条随机数据
//使用Random类,可以生成随机数
Random random = new Random();
//int i = random.nextInt(100000);//生成随机数
//System.out.println(i);
//声明一个长度为10000的数组
int[] arr = new int[10000];
for (int i = 0; i < arr.length; i++) {
//往数组中存入100000以内的数据
arr[i] = random.nextInt(100000);
}
//System.currentTimeMillis()可以放回当前时间的毫秒值
long start = System.currentTimeMillis();
System.out.println("排序前:");
//调用排序方法
//BubbleSort.bubbleSort(arr);
//InsertSort.insertSort(arr);
//QuickSort.quickSort(arr,0,arr.length-1);
//SelectSort.selectSort(arr);
System.out.println("排序后:");
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("排序时间:" + time);
}
}
//插入排序10万条数据时间:842 //快速排序10万条数据时间:15 //冒泡排序10万条数据时间:14005 //选择排序10万条数据时间:6464 //插入排序100万条数据时间:77875 //快速排序100万条数据时间:130 //冒泡排序100万条数据时间:大致三小时左右 //选择排序100万条数据时间:206812
6.3.5 查找算法
查找 :指的是在指定数组中,查找指定值,返回其下标的一种方式。
public class SearchDemo {
public static void main(String[] args) {
//定义一个数组
int[] arr = {12,34,56,23,11,66,39,36};
//查找39在数组中的什么位置?返回它的下标
//基本查找方式:
int index = -1;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == 39){
index = i;
break;
}
}
System.out.println("查找的39的下标:" + index);
}
}
6.3.6 二分查找
二分查找法 :也称为折半查找法,用来查找有序的序列数据
/*
二分查找
*/
public class BinarySearch {
public static void main(String[] args) {
//定义一个有序的数组
int[] arr = {11,22,33,44,55,66,77,88,99};
int index = getIndex(arr, 44);
System.out.println("44的下标是:" + index);
}
//写一个二分查找方法,返回值是 索引值
//参数是 数组和 要查找的值
public static int getIndex(int[] arr,int value){
//定义最小索引、最大索引
int max = arr.length -1 ;
int min = 0;
//计算中间索引
int mid = (max + min) / 2;
//拿查找的值和中间索引值做比较
//如果中间值和查找的值不同,就循环,相同,循环结束
while (value != arr[mid]){
//中间值大 ,往左找
if (arr[mid] > value){
max = mid - 1;
//中间值小,往右找
}else if (arr[mid] < value){
min = mid + 1;
}
//判断,没有找到的情况
if (min > max){
return -1;
}
mid = (max + min) / 2;
}
return mid;
}
}
练习:
数组中,存放了10位同学的平均成绩,其中张三的成绩是84分,其他人的平均成绩在70-100之间随机生成。
求:生成成绩后,张三的成绩在班级中的排名是多少?
public class Demo01 {
public static void main(String[] args) {
/*
数组中,存放了10位同学的平均成绩,其中张三的成绩是84分,
其他人的平均成绩在70-100之间随机生成
求:生成成绩后,张三的成绩在班级中的排名是多少
*/
//随机生成数组数据
int[] arr = new int[10];
arr[0] = 84;
//创建随机数对象random
Random random = new Random();
for (int i = 1; i < arr.length; i++) {
arr[i] = random.nextInt(30) + 70;
}
System.out.println("所有学生的平均成绩:");
BubbleSort.printArray(arr);
//数组排序
BubbleSort.bubbleSort(arr);
//数组查找
int index = BinarySearch.getIndex(arr, 84);
int paiming = arr.length - index;
System.out.println("张三的排名是:" + paiming);
}
}
6.3.7 Arrays工具类
jdk提供给我们使用的一个工具类,工具类中一般都封装了 很多的方法,可以给我们使用。
public class ArraysDemo {
public static void main(String[] args) {
int[] arr = {10,33,4,21,35,6};
//toString()方法, 将数组以字符串形式输出
System.out.println(Arrays.toString(arr));
Arrays.sort(arr); //调用排序方法
System.out.println(Arrays.toString(arr));
int i = Arrays.binarySearch(arr, 33); //搜索
System.out.println(i);
//复制数组
int[] arr01 = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(arr01));
char[] charArray = {'a','c','b','x','f'};
Arrays.sort(charArray);
System.out.println(Arrays.toString(charArray));
}
}
7 面向对象
7.1 面向对象
程序开发的过程:
早期,使用结构化程序设计语言, 随着软件规模的扩大,添加起来比较麻烦,导致开发会无限期的拖延。
所有程序设计者,就将面向对象的开发思想,引入到程序中。
7.1.1 面向对象VS面向过程
举个例子,来区分面向过程和面向对象
比如,去吃饭的例子,你想吃红烧肉
1, 自己买材料,自己做,最后吃饭
2,去饭店,点菜,等服务员上菜,然后吃饭
第一种方式,就是面向过程,我们自己在全程参与这个事情,缺少一个环节都不行
第二种方式,就是面向对象,我们只需要等待厨房把菜烧好了,吃就行了
面向对象的优势:
- 首先,我们不需要知道这个菜是怎么做的,降低了耦合性,如果不想吃这道菜了,想吃其他菜了
- 面向过程,需要重新买材料,重新做
- 面向对象,只要跟服务员说,将菜换掉
- 面向过程 : 是具体化的,流程化,解决一个问题,需要一步一步的分析、实现
- 面向对象: 是模型化的,只需要抽象一个类,在这个类中,有解决问题的方法,我们需要什么方法,直接调用就可以了,不需要一步一步去实现了。
优缺点 :
- 面向过程的优点:性能比面向对象好,因为面向对象中类在调用的时候,需要实例化,开销比较大,会消耗资源
- 面向过程的缺点 :不容易维护,不容易复用,不容易扩展
- 面向对象的优点 : 易维护、易复用、易扩展,面向对象具有封装、继承、多态的特性,可以设计出低耦合的系统。
面向对象的缺点: 性能比面向过程。
7.1.2 OOA OOD OOP
面向对象作为一种思想,为软件开发的整个过程做的事情:
从分析————>实现 ————>编写
面向对象简称是OO (Object Oriented),分别OOA OOD OOP
- OOA 面向对象的思想, 在分析软件开发需求的时候,就将面向对象中 类、对象的思想引入
- OOD 在OOA分析基础上,完成软件开发的相关决策,OOD应该划分出主要的系统模块以及接口等
- OOP 实现OOD规定的接口和模块(完成面向对象的编程)
7.1.3 类和对象的关系
- 面向对象中,两个最关键的词汇 : 类 和 对象
- 类 : 对象的抽象称为类,将具有共同属性的事物抽取出来的一个内容
- 对象 : 生活的事物,类的实例化,称为对象
类和对象在Java代码中的表示:
//电脑类
public class Computer {
//类的属性,就是特征,属性的写法,其实就是声明变量
//声明方式: 数据类型 属性名;
String brand; //品牌属性
String color; //颜色
double size; //尺寸
int memory; //内存
//功能,其实就是方法
//打游戏功能
public void playGame(){
System.out.println("电脑可以玩游戏!");
}
//看电影功能
public void movie(){
System.out.println("电脑可以看电影!");
}
//敲代码功能
public void code(){
System.out.println("电脑可以敲代码!");
}
public static void main(String[] args) {
//实例化对象 通过new关键字
//语法 : 类名 对象名(变量名) = new 类名();
Computer huashuo = new Computer();
huashuo.brand = "华硕";
huashuo.color = "黑色";
huashuo.size = 15.4;
huashuo.memory = 16;
System.out.println(huashuo.brand + "--"+
huashuo.color + "--"+
huashuo.size + "--" +
huashuo.memory);
huashuo.playGame();
}
}
7.2 类、属性、方法、对象的概念
Java中类、属性、方法、对象的创建方式
7.2.1类的创建:
class 类名{ //取名规范是大驼峰式
}
public class Animal {
}
7.2.2 属性的创建
属性就是类的特征,可以看成是类中的某个数据,是类的成员
写法:
数据类型 属性名(变量名);
属性在声明的时候,可以指定值,也可以不指定值,
如果指定了值,那么这个属性的默认值就是指定的值
如果没有指定值,那么这个属性的默认值是 不同类型对应的默认值
整数类 默认值 : 0
浮点数 : 0.0
char : 空格
引用类型 : null
public class Animal {
String type = "猫类";
char gender;
String name;
String color;
int age;
double price;
}
7.2.3 类的方法创建
类的方法其实是描述了类具有的功能(行为),是类的成员,将来在方法中,一般定义可以执行功能的代码,
将来这个类的对象,都可以拥有这个方法(功能)
public class Animal {
//类的成员方法
public void sleep(){
System.out.println("动物会睡觉!");
}
public void eat(){
System.out.println("动物会吃东西!");
}
}
7.2.4 对象的创建
类中的成员属性和 成员方法,必须要创建这个类的对象之后才能调用
语法 :
类名 对象名 = new 类名();
7.2.5 属性和方法的调用:
语法 :
对象名 . 属性名 = 值; //通过对象给属性赋值
对象名 . 方法名(); //通过对象调用方法
public static void main(String[] args) {
//创建对象,创建对象之后,才能使用类的属性和方法
//写法 :
Animal animal = new Animal();
//调用属性
animal.type = "猫类";
//调用方法
animal.eat();
}
练习 :
创建一个人类 ,人类有 姓名、年龄、性别三个属性
有一个自我介绍的方法,介绍内容是: 我是xxx,我今年xx岁,我的性别是:x
public class Person {
//属性
String name;
int age;
String gender;
//方法
public void show(){
System.out.println("我是" + name +
",我今年" + age + "岁,我的性别是" + gender);
}
}
public class Person {
//属性
String name;
int age;
String gender;
//方法
public void show(){
System.out.println("我是" + name +
",我今年" + age + "岁,我的性别是" + gender);
}
}
注意 :
1,类对象的创建,可以在类中的main方法中创建,也可以在别的类的main方法中创建
2,类的成员属性或者成员方法,必须要通过这个类的对象才能调用到
7.3 对象的内存图
- java文件被编译为.class的字节码文件,字节码文件会进入方法区
- 运行main方法之后,main方法会先进栈
- 执行main方法中的代码,从上到下,依次执行
- 先执行: Person person = new Person(); 会在堆区创建一个区域,将方法区的Person类及其属性方法都会拿过来,成员属性拿过来后有一个默认值,成员方法拿过来后其实是一个引用,指向方法区中的字节码文件中的方法
- 堆中空间创建好后,会将这个空间的地址值,赋值给 person变量
- 当我们通过person变量去调用属性赋值的时候,就会将引用的堆中空间里面的属性的默认值替换掉
- 当我们通过person变量去调用方法的时候,通过引用地址找到堆中的成员方法引用,找到要执行的方法后,把方法加载进栈
- 代码全部执行完后, 普通成员方法先出栈,main方法后出栈
7.4 成员变量和局部变量
- 成员变量指的是: 在类中成员位置声明的变量
- 局部变量指的是 : 在类中的方法里面声明的变量
区别 :
- 声明的位置不同 : 成员变量在 类中,方法外 局部变量 在类中的 方法内
- 初始值不同 :成员变量可以不同赋值,系统默认给初始值 局部变量使用前必须赋值
- 内存位置不同 : 成员变量 在堆中 ,局部变量在栈中
- 生命周期不同: 成员变量随着对象的创建而存在,对象的消失而消失 ,
- 局部变量随着方法的调用存在,方法调用结束而消失
7.5 构造函数
public class Student {
String name; //姓名
int age; //年龄
String className; //班级名称
//创建这个类的构造函数(构造方法)
//构造方法,没有返回值
//方法名需要和类名相同
public Student(String name,
int age,
String className){
this.name = name;
this.age = age;
this.className = className;
}
public Student(String name){
this.name = name;
}
public Student(){
}
//成员方法
public void show(){
System.out.println("我是" + name
+",我的年龄是:" + age + ",我的班级是" + className);
}
}
public class StudentTest {
public static void main(String[] args) {
// 创建对象的时候,使用new 关键字 后面跟上的这个 类名()其实就是构造方法
// 构造方法,它是和类名同名的
// 一个类在声明之后,系统就会默认提供一个无参构造方法
//其实我们在new 类名() 的时候,调用的就是这个无参构造
//我们一旦加了其他的构造方法,系统会将提供的无参构造回收,无参构造就无法使用了
//如果想无参构造能继续被使用,需要手动添加一个无参构造方法
Student s1 = new Student();
s1.name = "张三";
s1.age = 20;
s1.className = "1班";
s1.show();
Student s2 = new Student("李四",
22, "2班");
s2.show();
Student s3 = new Student("王五");
s3.show();
}
}
1,构造函数是什么?
- 构造函数,其实就是在使用new 关键字,创建类的对象的时候,调用的那个方法
- 构造函数就是用来实例化对象的时候被使用的,它必须要通过new关键字来调用
2,构造函数怎么创建
我们声明了类之后,系统就会默认的提供一个无参的构造函数,这个时候可以通过new 类名()的方式创建对象
如果我们在类中声明了构造函数,那么系统将会把这个无参构造回收掉,如果你还想使用无参构造,那么必须要手动创建它
构造函数的创建语法 :
public 类名(参数列表){
属性赋值;
}
3,构造函数的特点
- 构造函数和其他的普通方法是声明不一样,构造函数没有返回值,且名称必须和类名相同
- 构造函数的参数 ,可以有 0个 ,1个 或者多个
- 构造函数 将来需要配合new 关键字一起使用
- 创建类后,系统默认提供一个无参构造 ,如果自己定义了构造函数,系统不再提供
练习 :
- 创建一个手机类,类中有 品牌、颜色、价格三个属性,有一个介绍手机的方法,有一个全参的构造函数
- 创建两个手机对象,分别使用无参构造 和 全参构造 来完成创建
- 创建好对象后,通过对象,调用方法
public class Phone {
//属性
String brand;
String color;
double price;
//构造函数
public Phone(){} //无参
public Phone(String brand,String color,double price){
this.brand = brand;
this.color = color;
this.price = price;
}
//普通方法
public void show(){
System.out.println(brand + "--" + color + "--" + price);
}
}
public class PhoneTest {
public static void main(String[] args) {
//通过无参构造,构造对象
Phone phone = new Phone();
phone.brand = "苹果";
phone.color = "白色";
phone.price = 5999.0;
phone.show();
//通过全参构造
Phone phone1 = new Phone("华为", "银色", 6999.9);
phone1.show();
}
}
7.6 成员方法
- 成员方法,也称为实例方法,就是将方法声明在类中的成员位置
- 成员方法的语法,符合之前阶段学习的方法的写法
语法 :
修饰符 返回值类型 方法名(参数列表){
代码块;
return;
}
public class Phone {
//属性
String name;
String color;
double price;
//构造函数
public Phone(){} //无参
public Phone(String brand,String color,double price){
this.name = brand;
this.color = color;
this.price = price;
}
//普通方法
public void show(){
System.out.println(name + "--" + color + "--" + price);
}
//定义一个打电话方法,需要传入一个姓名,完成打电话操作
//输出,使用 xx 品牌手机给xxx打电话
public void call(String name){
System.out.println("使用" + this.name +"手机给" + name + "打电话!");
}
//计算功能 ,计算 两个数字的和 ,有返回值,有参数的方法
public int operator(int a,int b){
return a + b;
}
}
public class PhoneTest {
public static void main(String[] args) {
//通过无参构造,构造对象
Phone phone = new Phone();
phone.name = "苹果";
phone.color = "白色";
phone.price = 5999.0;
phone.show();
phone.call("jack");
//通过全参构造
Phone phone1 = new Phone("华为", "银色", 6999.9);
phone1.show();
//调用成员方法
phone1.call("张三");
int i = phone1.operator(10, 20);
System.out.println(i);
}
}
7.7 参数传递
指的是,在调用方法的时候,传入参数的数据类型的不同,可能会得到不同的结果
方法的参数类型:
总的来说,传入的参数只有两种类型 : 基本数据类型 和 引用数据类型
Java中,参数传递只有一种方式,按值传递
基本类型,就是传递自身的值
引用类型,传递的是对应的地址值,不是对象自身
- 基本类型值传递
/*
基本类型的值传递
*/
public class Demo01 {
public void change(int a){
a = 100;
}
public static void main(String[] args) {
int a = 200;
Demo01 d = new Demo01();
d.change(a);
System.out.println(a); //200
}
}
- 对象值传递
public class Demo02 {
public void change(Person person){
person.age = 20;
}
public static void main(String[] args) {
Person person = new Person(); //4554617c
System.out.println(person);
person.age = 10;
System.out.println(person.age);//10
Demo02 demo02 = new Demo02();
//方法参数,是引用类型的话,会将引用地址中的内容改变
demo02.change(person);
System.out.println(person.age); //20
}
}
public class Person {
int age;
}
- 字符串的值传递
public class Demo03 {
public void change(String str){
str = "hello";
}
//字符串是一个特殊的引用类型,它作为参数传递的时候
//和基本类型的传递方式一致
public static void main(String[] args) {
//String str = "你好";
String str = new String("你好");
System.out.println(str);//你好
Demo03 demo03 = new Demo03();
//将字符类型作为参数传递
demo03.change(str);
System.out.println(str); //你好
}
}
- 数组的值传递
public class Demo04 {
public void change(int[] arr){
arr[0] = 20;
}
//数组作为参数传递,会改变引用的内容
public static void main(String[] args) {
int arr[] = {10};
System.out.println(arr[0]);//10
Demo04 demo04 = new Demo04();
demo04.change(arr);
System.out.println(arr[0]);//20
}
}
7.8 方法重载
概念 :
- 在同一个类中,方法名相同,参数列表不同(包括 参数的个数 、参数是顺序、参数的类型不同),和返回值以及访问修饰符无关,称为方法重载
- 方法重载,包括 : 构造方法重载 、普通方法重载
- 方法重载的调用 : 根据你调用方法的时候,传入的实际参数去判断,到底调用哪个方法
public class Student {
String name; //姓名
String sid; //学号
//构造方法重载
public Student(){
}
public Student(String name){
this.name = name;
}
public Student(String sid,String name){
this.sid = sid;
this.name = name;
}
//普通方法的重载
public void add(int a,double b){
System.out.println(a + b);
}
//类型不同
public int add(int a,int b){
return a+b;
}
//顺序不同
public double add(double b,int a){
return a+b;
}
//个数不同
public int add(int a,int b,int c){
return a+b+c;
}
public static void main(String[] args) {
Student s = new Student("张三");
//调用重载方法
s.add(10,20);
s.add(1,2,3);
}
}
7.9 this关键字
- this关键字,表示的是当前对象的意思
- this可以用来调用属性、调用方法、调用构造方法
7.9.1 this调用属性
- this调用属性的时候,指向的是当前对象
public class Person {
String name;
//和谁聊天的方法
//this表示当前对象的意思,指向方法的当前调用对象
//谁调用,就指向谁
//this在方法中使用的时候,如果方法中存在和属性同名的局部变量
//加上this可以表示指向对象的属性
public void say(String name){
System.out.println(this.name + "和" + name +"正在聊天!");
}
public Person() {
}
//构造方法中的this,在调用属性的时候
//表示,将来通过这个构造方法创建的对象的属性的值就是传入的值
//this就是指向创建的对象
public Person(String name) {
this.name = name;
}
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "张三";
p1.say("王五"); //张三和王五在聊天
Person p2 = new Person();
p2.name = "李四";
p2.say("王五"); //李四和王五在聊天
Person p3 = new Person("张无忌");
Person p4 = new Person("赵敏");
}
}
7.9.2 this调用成员方法
public class Student {
public void say(){
System.out.println("学生在说话!");
}
public void say(String name){
System.out.println("学生在和" + name + "说话");
}
public void sleep(){
say();
//this调用当前类的成员方法的时候,可以省略不写
// this.say();
this.say("张三");
// System.out.println("学生在说话!");
System.out.println("学生在睡觉!");
}
public static void main(String[] args) {
Student s = new Student();
s.sleep();
}
}
7.9.3 this调用构造方法
- this调用构造方法的时候,只能在构造方法中的第一行调用
public class Demo01 {
String name;
int age;
public Demo01(){
this("jack"); //调用的是name参数的构造方法
System.out.println("这是无参构造!1");
}
public Demo01(String name) {
this(20); //这个调用的是age参数的构造
System.out.println("这是参数为name的构造方法!2");
}
public Demo01(int age) {
this.show();//调用的 普通方法show
System.out.println("这是参数为age的构造方法!3");
}
public void show(){
//普通方法中,不能通过this调用构造方法
//this("jack");
//普通方法中,创建通过new关键字创建对象是允许的
Student s = new Student();
System.out.println("这是普通方法!4");
}
public static void main(String[] args) {
new Demo01(); //
}
}
7.10 封装与包
7.10.1 封装的定义、封装的使用、封装的目的
定义:
- 封装其实是对象的一种隐藏技术,目的是将对象中的属性和方法组织起来,同时隐藏不想暴露的属性和方法的实现细节
- 用户或者其他对象就不能看到,也不能修改这些被隐藏的实现
- 将来只能通过指定的接口去调用对象的方法,从而达到通信的目的。
目的:
- 为了将设计者和使用者分开,使用者不需要知道实现的细节,只需要设计者提供的方法来访问对象就可以了
使用:
- 封装通过private关键字来使用,将这个关键字写在需要封装的属性或者方法之前即可
问题: 私有化之后的属性,无法被对象直接调用,怎么完成赋值和 值的获取?
对象属性的赋值 :
- 通过 全参的构造方法 赋值
- 通过在类中设定 set方法,对象调用set方法,来完成对象的赋值
获取对象的属性值:
- 在别的类中,想要获取属性的值,只能通过get方法来获取
总结 :
- 属性在私有化之后,一般需要给属性指定getter/setter方法
- 如果类和类之间需要产生属性的数据交互,那么就可以通过 getter/setter方法
public class School {
//属性
private String schoolName;
private int studentNumber;
private int teacherNumber;
private int classNumber;
//set方法,就是普通方法 ,返回值为空,需要传入参数用来设置属性值
public void setSchoolName(String schoolName){
this.schoolName = schoolName;
}
public void setStudentNumber(int studentNumber){
this.studentNumber = studentNumber;
}
public void setTeacherNumber(int teacherNumber){
this.teacherNumber = teacherNumber;
}
public void setClassNumber(int classNumber){
this.classNumber = classNumber;
}
//声明get方法,方法存在返回值,方法没有参数,方法中有return
public String getSchoolName(){
return schoolName;
}
public int getStudentNumber(){
return studentNumber;
}
public int getTeacherNumber(){
return teacherNumber;
}
public int getClassNumber(){
return classNumber;
}
//构造方法
public School(String schoolName, int studentNumber, int teacherNumber, int classNumber) {
this.schoolName = schoolName;
this.studentNumber = studentNumber;
this.teacherNumber = teacherNumber;
this.classNumber = classNumber;
}
public School() {
}
//定义一个普通方法,将对象的信息展示
public void show(){
System.out.println("学校的名称:" + schoolName +
",学校的学生数量:" + studentNumber +
",学校的老师数量:" + teacherNumber +
",学校的教室数量:" + classNumber);
}
public static void main(String[] args) {
//在同一个类的main方法中,对象是可以正常调用属性的
School school = new School();
school.schoolName = "南京大学";
System.out.println(school.schoolName);
}
}
public class SchoolTest {
public static void main(String[] args) {
School s1 = new School();
//private私有化的属性,在不同类中,属性不能被对象直接调用
//s1.schoolName = "南京邮电大学";
//创建对象,并且给对象赋值的方式:
//1,通过全参的构造方法
School s2 = new School("南京邮电大学",
8000, 1000,
2000);
//赋值之后的对象的私有化的属性,仍然无法直接调用
//System.out.println(s2.schoolName);
//对象的普通方法中,可以接收到对象的赋值
s2.show();
//2,通过setter方法去完成属性的赋值
School s3 = new School();
s3.setSchoolName("南京大学");
s3.setStudentNumber(10000);
s3.setTeacherNumber(2000);
s3.setClassNumber(4000);
s3.show();
//想去获取到某个属性值,不能直接通过点的方式拿到
//也可以通过在类中声明一个普通方法,将属性值返回
//通过设置getter 方法将,属性返回
String schoolName = s3.getSchoolName();
System.out.println(schoolName);
System.out.println(s3.getStudentNumber());
System.out.println(s3.getTeacherNumber());
System.out.println(s3.getClassNumber());
}
}
- 私有化之后,多个对象之间的属性值交互的写法
public class Student {
//属性 学号、姓名、班级
private int sid;
private String sName;
private String sClass;
private String[] course;
//构造方法
public String[] getCourse() {
return course;
}
public void setCourse(String[] course) {
this.course = course;
}
public Student() {
}
public Student(int sid, String sName, String sClass) {
this.sid = sid;
this.sName = sName;
this.sClass = sClass;
}
//get/set方法
public int getSid() {
return sid;
}
public void setSid(int sid) {
this.sid = sid;
}
public String getsName() {
return sName;
}
public void setsName(String sName) {
this.sName = sName;
}
public String getsClass() {
return sClass;
}
public void setsClass(String sClass) {
this.sClass = sClass;
}
//将对象以字符串的形式输出
@Override
public String toString() {
return "Student{" +
"sid=" + sid +
", sName='" + sName + '\'' +
", sClass='" + sClass + '\'' +
", course=" + Arrays.toString(course) +
'}';
}
}
//课程类
public class Course {
private String name;
public Course(String name) {
this.name = name;
}
public Course() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//测试类
public class StudentTest {
public static void main(String[] args) {
//创建student对象s1
Student s1 = new Student();
//s1调用set方法设置属性
s1.setsName("张三");
s1.setSid(1001);
s1.setsClass("1班");
//课程名称,需要从Course对象获取
//创建Course对象,并指定每个课程对象对应的课程名
Course c1 = new Course("语文");
Course c2 = new Course("数学");
Course c3 = new Course("英语");
//设置s1对象的课程,需要调用已知的课程对象的名称,放入课程数组中
//课程对象的名称,就得通过get方法来拿到
String[] course = {c1.getName(),c2.getName(),c3.getName()};
s1.setCourse(course);
//输出s1对象
System.out.println(s1);
}
}
7.19.2 包的引用、常用包
包: 称为package,就是用来存放类的文件夹
一般,在创建类的时候,一般在类文件的第一行出现,内容是这个类存在于项目中src这个目录下的所有路径
包的取名 :一般包是以倒置公司的网络域名作为前缀,再加上项目名。包一般都使用的小写的名字
常用包 : java.util java.sql java.io
导包: 使用import 关键字将包导入
导入相同类名,不同包的类时候,需要使用其中一个包名作为前缀,用来区别不同的类
7.11 访问控制符
7.11.1 访问控制修饰符
Java中,访问控制修饰符,用来保护 类、变量、方法、构造方法被访问的情况
- private :私有的 ,在同一个类中,是可以被调用的,不能用来修饰类
- default :默认,什么都不写 ,在类的同一个包下可以被访问,可以修饰 : 类、变量、方法、构造方法
- protected :受保护的 ,同一个包下可以被访问,或者其子类都可以访问,不可以用来修饰类,可以用来修饰 变量、方法
- public :公共的,所有位置都可以访问
7.11.2 非访问控制修饰符 static
常用的非访问修饰符 ,static、final 、 abstract 、synchronized 、volatile
- static修饰符,表示静态的,可以用来修饰类中的 方法和变量,被static修饰后的方法称为类方法,被static修饰过的变量,称为类变量,还可以修饰代码块
static修饰成员变量
- static修饰的属性,是类的所有对象共享的,可以用来在对象之间共享数据
- 将来如果类中所有的对象都包含一个相同属性值的变量,可以把这个属性定义为静态的,从而节省内存空间
public class Student {
String name;
int age;
static String className; //静态的属性,对象共享
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", className=" + className +
'}';
}
}
public class StudentTest {
public static void main(String[] args) {
System.out.println(Student.className);
//System.out.println(Student.name);
Student s1 = new Student();
s1.name = "张三";
s1.age = 20;
s1.className = "1班";
System.out.println(s1);
Student s2 = new Student();
s2.name = "李四";
s2.age = 22;
System.out.println(s2);
Student s3 = new Student();
s3.name = "王五";
s3.age = 21;
System.out.println(s3);
Student s4 = new Student();
s4.name = "王五";
s4.age = 21;
s4.className = "2班";
System.out.println(s4);
}
}
public class StaticDemo {
static int num;
public static void main(String[] args) {
num++;
StaticDemo s1 = new StaticDemo();
s1.num++;
StaticDemo s2 = new StaticDemo();
s2.num++;
StaticDemo.num++;
System.out.println(num); //4
}
}
static修饰成员方法
static修饰方法,随着类的加载而加载,比如main方法,优先于对象存在,即使没有创建这个类的对象,也可以通过类名调用类中的static方法,被类的所有对象共享
- 静态方法中,只能访问静态成员变量和 静态方法
public class StaticDemo01 {
//普通变量
int number01 = 100;
//静态变量
static int number02 = 200;
public void say(){
}
public static void show(){
//静态方法中,不能使用非静态的成员变量
//System.out.println(number01);
System.out.println(number02);
//静态方法中,也不能调用非静态的成员方法
//say();
}
//main方法也是静态方法
public static void main(String[] args) {
// say();
// System.out.println(number01);
System.out.println(number02);
show();
}
}
static修饰代码块
static修饰的代码块称为静态代码块
代码块是什么 ?
在Java中,使用{}括起来的代码,就被称为代码块。代码块分类如下 :
- 局部代码块:声明在局部位置,用于限定变量的生命周期
- 构造代码块:声明在类中的成员位置,一般在调用构造方法之前会执行构造代码块
- 静态代码块: 声明在类中的成员位置,多了static修饰符,一般类被加载的时候就会执行,只会执行一次
public class StaticDemo02 {
//静态代码块
static {
int a = 100;
System.out.println(a);
}
//构造代码块
{
int b = 200;
System.out.println(b);
}
public StaticDemo02(){
System.out.println("构造方法");
}
{
int c = 300;
System.out.println(c);
}
static {
int d = 400;
System.out.println(d);
}
}
public class StaticTest {
public static void main(String[] args) {
{
int e = 500;
System.out.println(e);
}
StaticDemo02 s1 = new StaticDemo02();
//500
//100
//400
//200
//300
//构造方法
System.out.println("----------------------");
StaticDemo02 s2 = new StaticDemo02();
//200
//300
//构造方法
}
}
7.12 继承的定义、继承的好处、如何继承、继承的特征
定义 :
- 把多个类中,相同的成员提取出来,定义到一个独立的类中。
- 然后让当前类和独立类之间产生关系,使当前类具有独立类中的成员内容,这种关系,称为继承
- 有了继承关系后,当前类称为子类,独立类称为父类
写法:
- 用extends 关键字来实现继承
- class 子类名 extends 父类名{}
public class Person {
String name; //姓名
int age; //年龄
String gender; //性别
public void show(){
System.out.println("我的姓名:" + name
+ ",我的年龄:" + age
+ ",我的性别:" + gender);
}
}
public class Student extends Person {
String className; //班级
}
public class Emp extends Person{ //员工类
String deptName; //部门名称
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
//调用来自Person中的属性
s1.name = "张三";
s1.age = 20;
s1.gender = "男";
//调用来自Person类中的方法
s1.show();
Emp emp = new Emp();
emp.name = "李四";
emp.age = 30;
emp.gender = "男";
emp.show();
}
}
继承的优点:
- 提高代码的复用性,也可以提高代码的维护性
- 继承让类与类之间产生了一个关系,是将来学习的多态的前提
继承的缺点 :
- 继承会让类的耦合性增强,这样父类改变后,就会影响跟他相关的类
- 打破封装性,子类可以通过super访问父类的构造方法,从而完成私有属性的赋值,也可以通过继承父类的get/set方法,完成父类私有属性的赋值、调用
继承的其他特点:
- Java中只能单继承,只能继承一个类
- Java中可以多重继承
什么时候可以使用继承?
两个类之间 存在 : is a 的关系
public class Person {
private String name; //姓名
private int age; //年龄
private String gender; //性别
//父类的属性私有化之后,子类可以通过构造方法、get/set方法
//间接的访问到父类的私有属性
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Person() {
}
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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public void show(){
System.out.println("我的姓名:" + name
+ ",我的年龄:" + age
+ ",我的性别:" + gender);
}
}
public class Student extends Person{
String className; //班级
//如果想要使用父类的构造方法,必须在子类中定义自己的构造
//然后在通过特殊方法,super关键字,去访问父类的构造方法
public Student(String name, int age, String gender) {
//super表示父类的意思,super()表示访问父类的构造方法
super(name, age, gender);
}
public Student(){}
}
public class Test {
public static void main(String[] args) {
//如果父类的属性,私有化之后,那么就不能被直接继承
//父类属性私有化之后,创建子类对象,怎么使用父类的私有属性
//1,子类的构造方法通过super访问父类的构造方法之后,
//就可以被拿来实例化对象的时候赋值使用了
Student s2 = new Student("张三", 20, "男");
s2.show();
//2,子类调用从父类继承过来的 getter/setter方法,完成赋值和调用
Student s3 = new Student();
s3.setName("李四");
s3.setAge(20);
s3.setGender("男");
s3.show();
System.out.println(s3.getName());
}
}
继承之后的成员关系 :
1,成员变量
- 如果子类 成员变量名 和 父类成员变量名 不一样,子类调用的就是子类中的成员变量
- 如果子类 成员变量名 和父类的成员变量名 一样,子类调用的其实也是子类自己的成员变量
2,成员方法
- 子类的成员方法和父类的成员方法不一样,子类调用 就是调用自己的方法
- 子类的成员方法和父类的成员方法同名,子类调用的还是自己的 (先找自身,再找父类)
3,构造方法
- 子类会默认去访问父类的无参构造,如果父类没有无参构造,那么,会报错,
如果报错(父类没有无参构造)怎么解决 ?
- 给父类加上无参构造
- 父类没有无参构造,那么必定存在其他的有参构造,子类通过super去访问父类的有参构造,这个时候,子类也只能通过有参构造去创建对象
public class Person {
String name; //姓名
int age; //年龄
String gender; //性别
//父类的无参构造被注释,子类就无法使用父类的无参构造了
//public Person() {
//}
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public void show(){
System.out.println("我的姓名:" + name
+ ",我的年龄:" + age
+ ",我的性别:" + gender);
}
}
public class Student extends Person{
String name;
String className; //班级
//如果父类不提供无参构造,那么子类也不能使用无参构造
//public Student() {
//}
public Student(String name, int age, String gender, String name1, String className) {
super(name, age, gender);
this.name = name1;
this.className = className;
}
public void say(){
System.out.println("子类的say方法");
}
//public void show(){
// System.out.println("子类的show方法");
//}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
//子类指定了跟父类同名的属性,赋值的时候,
// 其实赋值的是自己子类中的属性
s1.name = "张三";
//调用来自Person中的属性
s1.age = 20;
s1.gender = "男";
s1.className = "1班";
//调用来自Person类中的方法
s1.show(); //我的姓名:null,我的年龄:20,我的性别:男
s1.say(); //自己调用和父类不同名方法,在自己类找
//s1.show(); //子类调用同名方法,优先调用自己
}
}
继承总结:
- 子类继承父类,被继承的类称为父类、超类、基类,可以继承父类的所有属性和方法(私有的可以间接的继承,不能直接访问)
- Java中一个类只能继承一个类,可以多重继承,从而实现继承多个类的效果
- 如果创建了一个类,没有写继承,那么这个类其实是继承了Object类,Object是所有类的始祖类
- 子类可以扩展属性、方法
- 继承提升代码的复用性
7.13 supper关键字
定义 :super在调用的时候,指的是当前类对象的父类
- 因为父类的属性私有化之后,不能直接被子类调用,这时候就可以通过super关键字完成调用
- super可以用来调用父类的属性、成员方法、构造方法
- super只能出现在子类的普通方法和构造方法中
- super在子类中构造方法中,只能出现在第一行代码中
子类通过super调用父类的属性:
super调用父类的属性,不能调用父类的私有属性,所以,将来用super调用父类属性的用法其实很少用,只能调用没有私有化的属性
子类通过super调用父类的构造方法
写在子类构造方法中的第一行
写法 : super();
1.关于无参构造的调用,子类的无参构造会默认访问父类的无参构造
- 如果子类的有参构造,没有显示的访问父类的有参构造,其实也会默认访问父类的无参构造
2.为什么?
- 因为子类继承父类、可能会使用父类的数据,所以在子类初始化之前,得先完成父类数据的初始化
2.关于有参构造的调用
- 子类可以通过在super()中传入参数的形式,完成对父类有参构造方法的调用
- 子类通过super调用父类的普通方法
- 子类可以在构造方法或者普通方法中,通过super调用父类的普通方法
写法 : super.方法名();
public class Person {
String name; //姓名
int age; //年龄
String gender; //性别
//父类的无参构造被注释,子类就无法使用父类的无参构造了
public Person() {
}
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void show(){
System.out.println("我的姓名:" + name
+ ",我的年龄:" + age
+ ",我的性别:" + gender);
}
}
public class Emp extends Person{ //员工类
String deptName; //部门名称,一种扩展
public Emp() {
//子类的无参构造,默认会去访问父类的无参构造
super();
super.show();
}
public Emp(String name, int age, String gender, String deptName) {
super(name,age,gender);
//super(name, age);
this.deptName = deptName;
}
public void show() {
// 通过super调用父类的普通方法
super.show();
System.out.println("我所在的部门:" + deptName);
}
}
public class TestEmp {
public static void main(String[] args) {
Emp emp = new Emp();
emp.deptName = "研发部";
emp.name = "张三";
emp.age = 20;
emp.gender = "男";
emp.show();
Emp emp1 = new Emp("李四", 22, "男", "财务部");
emp1.show();
}
}
练习:
创建一个动物类,作为父类 ,有私有化 种类、颜色两个属性,有一个show方法,作为动物的介绍
介绍内容为 : 我的种类是:xx,我的颜色是:xx
声明一个 小狗类,作为子类,有姓名 私有化属性作为扩展 ,子类也有一个show方法,并对父类的show方法做一个扩展,添加一条输出语句: 我的名字是:xx
要求 :通过全参的构造方法,实例化一个狗类对象,并调用show方法
//动物类
public class Animal {
private String type;
private String color;
public Animal(String type, String color) {
this.type = type;
this.color = color;
}
public void show(){
System.out.println(" 我的种类是:" + type+
",我的颜色是:" + color);
}
}
public class Dog extends Animal {
private String name;
public Dog(String type, String color, String name) {
super(type, color); //通过super访问父类的构造方法
this.name = name;
}
public void show(){
super.show(); //调用父类的show方法
System.out.println("我的名字是:" + name);
}
}
public class DogTest {
public static void main(String[] args) {
Dog dog = new Dog("哈士奇", "白色", "二哈");
dog.show();
}
}
(重点)super和this的区别
this指的是当前对象的引用,super指的是父类对象引用
super关键字:
- super.父类属性名: 调用父类的属性
- super.父类方法名: 调用父类的方法
- super() : 调用父类的无参构造函数
- super(参数): 调用父类额有参构造函数
this的用法 :
- this.属性名 :调用当前对象的属性 (局部变量和成员变量冲突)
- this.方法名 : 调用当前对象的方法
- this() : 调用当前类的无参构造
- this(参数) : 调用当前类的有参构造
如果构造函数的第一行不是this()或者super()。系统默认添加super()
this和super关键字,不能共存
7.14 方法重写的定义
概念:
子类继承父类,子类重写父类的方法,方法名相同,参数列表相同,返回值类型相同或者是父类方法返回值的子类,访问修饰符不能严于父类,不能抛出比父类更多的异常
父类的私有方法不能被重写
父类的静态方法,不能被子类重写,
如果子类定义了和父类相同的静态方法或者私有方法,都会被当做子类自己的方法
子类为什么要重写父类方法?
一般是因为父类方法的功能不能满足子类的需求,然后子类对于父类方法做出扩展
public class Father {
public void method01(){
System.out.println("这是父类的普通方法");
}
public Father method02(){
return new Father();
}
public void method03(int a,int b){
System.out.println("父类中带参数的方法");
}
public static void method04(){
System.out.println("父类的静态方法");
}
private void method05(){
System.out.println("父类的私有方法");
}
}
public class Son extends Father {
@Override
public void method01() {
System.out.println("这是子类的方法");
}
//子类重写的方法的返回值,可以和父类相同
//或者是父类返回值的子类
@Override
public Son method02() {
return new Son();
}
//@Override //用来标识子类的方法重写了父类方法的
//method03参数和父类的参数不一样,这个方法仍然可以声明
//但是这个时候,它和父类方法没有关系,
// 只是子类自己的一个普通方法
public void method03(int a) {
System.out.println("子类的方法");
}
//@Override
//子类可以声明和父类相同名称的静态方法,
//但是它们之间的关系并不是重写,
//一般子类很少去写和父类同名的静态方法,除非想要将
//父类的静态方法在子类中隐藏
public static void method04(){
System.out.println("子类的静态方法");
}
//@Override
//父类的私有方法不能被子类重写
private void method05(){
System.out.println("子类重写父类的私有方法");
}
}
7.14.1 方法重写的应用
7.14.1.1 equals()方法重写
public class Student {
private int sid; //学号
private String name; //姓名
private String className; //班级
public Student() {
}
public Student(int sid, String name, String className) {
this.sid = sid;
this.name = name;
this.className = className;
}
@Override
public boolean equals(Object obj) {
//如果比较的两个对象,地址值相同,它们就是同一个对象
if (this == obj){
return true;
}
//如果传入的参数,不是一个Student对象,直接返回false
//通过 instanceof 可以判断某个对象是否是指定类的实例
// 用法 : 对象名 instanceof 类
if (!(obj instanceof Student)){
return false;
}
//判断传入的对象的属性和当前对象的属性的值,是否都是相同的
//传入的参数会被当做是Object类型,将传入的参数,转为Student
Student o = (Student) obj;
if (this.sid == o.sid &&
this.name.equals(o.name) &&
this.className.equals(o.className)){
return true;
}
return false;
}
@Override
public String toString() {
return "Student{" +
"sid=" + sid +
", name='" + name + '\'' +
", className='" + className + '\'' +
'}';
}
}
public class StudentTest {
public static void main(String[] args) {
//创建两个相同对象
Student s1 = new Student(10010, "张三", "1班");
Student s2 = new Student(10010, "张三", "1班");
System.out.println(s1);
//使用== 比较引用类型,比较的是地址值,返回false
System.out.println(s1 == s2); //false
//在Object类中,提供了一个equals()方法,用来比较对象之间是否相同的
//将来,如果想比较对象是否相同,那么就得用equals()方法
//两个相同的对象,我们调用equals比较,希望返回true,但是返回false
//这是因为Object类中的equals()方法,底层代码还是== 做比较,
//如果想要equals()方法可以比较对象的具体的属性,就需要重写equals()方法
System.out.println(s1.equals(s2));
}
}
7.14.1.2 toString()方法重写
当对象在被输出的时候,默认的会访问这个对象类的toString()方法,如果类中没有,就默认访问Object类中的toString()方法。
Object类中的toString()方法如下,表示返回当前类的包名+类名 +@+当前对象hashCode值的16进制
所以输出对象的时候,最后的结果就是一串值
所以将来,类中,一般都会重写toString()方法
相关面试题:
1 重载与重写的区别?
2,==和equals()方法区别
== 可以用来比较基本类型和引用类型
比较基本类型的时候,比较的是值
比较引用类型的时候,比较的是对象的地址值
equals()方法,是Object类中的方法,一般用来比较对象是否相同
底层代码其实还是 ==,所以如果想要使用equals()方法,一般会重写equals()方法
重写就是将对象的属性挨个比较,看看是否相同,重写之后可以用来比较对象是否相同
String类的equals()方法
重写了Object类中的equals()方法,底层的原理就是将需要比较字符串转为字符数组之后,利用循环,一个个比较数组中的字符值是否相同
3,为什么重写equals()方法,一定要重写hashCode()方法?
因为在Java中,相同的对象,它的hashCode值必须也是相同的
所以在重写equals()方法的同事,要重写hashCode()方法,为了保证他们的hashCode值也相同
7.15 抽象类
概念 :
继承中,将多个共同的内容,提取到一个类中,但是有些共同方法,方法声明是一样的,具体的实现不一样(每个具体的对象在做具体的操作的时候,功能实现不同),如果有这种情况,这个方法就需要被定义为抽象方法
抽象方法就是具有方法名,但是没有具体的方法实现(没有大括号)
如果一个类中存在抽象方法,那么该类 就是抽象类
7.15.1 抽象类特点
写法 : 抽象方法或者抽象类通过abstract 关键字 修饰
特点 :
抽象类不能实例化
抽象类中,可以没有抽象方法,但是有抽象方法的类一定是抽象类
抽象类的子类,必须是一个抽象类 或者 是个普通类,普通类必须重写抽象类中的所有抽象方法
抽象类的成员特点:
成员属性 : 可以有变量,也可以有常量
成员方法 :有抽象方法,普通方法,静态方法
构造方法 :有构造方法,抽象类不能实例化,所以构造方法一般提供给子类访问使用
public abstract class Animal {
private String name;
final int age = 20;
public Animal(String name) {
this.name = name;
}
public Animal() {
}
//抽象方法,声明为抽象方法的类,必须是抽象类
// 加上abstract关键字,完成方法和类的声明
public abstract void eat();
public void sleep(){
System.out.println("动物会睡觉!");
}
//抽象类中可以有静态方法
public static void show(){
System.out.println("动物会说话!");
}
}
//子类继承抽象类之后,子类要么是抽象类,要么就要实现父类中的所有抽象方法
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼~");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头~");
}
}
public class AnimalTest {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
cat.sleep();
cat.show();
Dog dog = new Dog();
dog.eat();
dog.sleep();
dog.show();
//抽象类不能实例化
//Animal animal = new Animal();
//抽象类中可以定义静态方法,也可以通过类名直接调用
Animal.show();
}
}
7.16 多态
多态的概念 :
一个对象,在不同时刻体现出来的不同状态
Java中多态的表示方式 :父类的引用指向子类的对象
父类 对象名 = new 子类();
public class Person {
public void say(){
System.out.println("人类会说话!");
}
public void show(){
System.out.println("这是父类的show方法");
}
}
public class Student extends Person {
@Override
public void show(){
System.out.println("这是子类的show方法");
}
public void show1(){
System.out.println("这是子类特有的show1方法");
}
}
public class PersonTest {
public static void main(String[] args) {
//多态创建Person对象
//父类的引用指向子类的对象
Person person = new Student();
person.say(); //作为父类形态,可以调用父类特有的方法
// person.show1(); //不能调用子类特有的方法
person.show(); // 作为子类形态,可以调用子类重写父类的方法
}
}
7.16.1 多态的特点
前提条件 :
1,有继承或者实现关系
2,有方法的重写
3,有父类或者父接口的引用指向子类对象
多态的分类 :
1,具体类的多态
class Fu{}
class Zi extends Fu{}
Fu f = new Zi();
2,抽象类的多态
abstract class Fu{}
class Zi extends Fu{}
Fu f = new Zi();
3,接口多态
interface Fu{}
class Zi implements Fu{}
Fu f = new Zi();
多态关系中成员访问特点 :
成员变量 : 直接看等号的左边,左边是谁,优先找谁,没有向上找,找不到就报错
编译看左边,运行看左边
成员方法 :先从等号的左边找,左边不存在,就报错,运行的时候看new的是谁,就找谁
编译看左边,运行看右边
构造方法 :子类的构造默认会访问父类构造
多态的好处:
多态可以提高代码的维护性(继承)
多态可以提高代码扩展性(多态体现)
public abstract class Emp {
public abstract void work();
}
public class Teacher extends Emp{
@Override
public void work() {
System.out.println("老师在讲课!");
}
}
public class Assistant extends Emp {
@Override
public void work() {
System.out.println("助教在辅导");
}
}
public class EmpTest {
public static void main(String[] args) {
//没有多态,创建对象,关心的是通过哪个类创建的对象调用的work方法
Teacher emp1 = new Teacher();
emp1.work();
Assistant emp2 = new Assistant();
emp2.work();
//多态的方式创建,关注点是work方法的调用,其他的东西我不关心
Emp emp3 = new Teacher();
emp3.work();
Emp emp4 = new Assistant();
emp4.work();
}
}
多态的缺点 :
父类引用不能使用子类特有功能,子类可以当做父类使用,父类不能当做子类使用
多态的转型:
多态中,父类引用不能使用子类特有功能,所以,要将父类对象做转型。
多态的转型,分为向上转型和向下转型
多态的分类 :
面向对象中,根据代码执行时刻的不同,将多态分为 编译时多态 和 运行时多态
编译时多态是静态的,一般指的是方法的重载,根据参数列表来区分不同的方法,通过编译之后就会变成不同的方法。
运行时多态是动态的,它是通过动态绑定来实现的,通过方法的重写来体现的,也就是之前讲解的面向对象的多态性
练习:
请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。
创建一个Bird类继承Animal类,重写eat()方法输出一条语句“鸟儿 吃虫”,特有方法fly()输出鸟儿飞翔"
在Test类中向上转型创建bird对象,调用eat()方法。然后向下转型调用fly()方 法。
public class Animal {
public void eat(){
System.out.println("吃东西!");
}
}
public class Bird extends Animal {
@Override
public void eat() {
System.out.println("鸟儿吃东西!");
}
public void fly(){
System.out.println("鸟儿飞翔!");
}
}
public class Test {
public static void main(String[] args) {
Animal bird = new Bird();
bird.eat();
Bird b = (Bird)bird;
b.fly();
}
}
7.17 final关键字
概念 :final表示最终的意思,可以用来修饰类、方法、变量
特点 :
1,final修饰的类,不能被继承 (String类能不能被继承?)
2,final修饰的方法,该方法不能被重写
3,final修饰的变量,该变量不能被重新赋值,final修饰的变量就是常量,一般final修饰的变量需要指定初始值。
public class Father {
public int num = 10; //普通的变量
public final int num2 = 20; //常量
public final void test01(){
System.out.println("父类的final方法");
}
}
public class Son extends Father {
String name;
public void show(){
num = 100;
//num2是常量,无法重新赋值
//num2 = 1000;
System.out.println(num);
System.out.println(num2);
}
//子类不能重新父类的final方法
//public void test01(){
// System.out.println("子类的方法");
//}
}
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.show();
final int a = 10; //final修饰的局部变量也是常量
// a = 100; //不能被重新赋值
System.out.println(a);
Son son1 = new Son();
son1.name = "张三";
System.out.println(son1.name);
final Son son2 = new Son();
son2.name = "李四";
System.out.println(son2.name);
//final修饰的引用类型变量,变量中的属性值是可以被改变的
son2.name = "王五";
System.out.println(son2.name);
//final修饰的引用的类型的变量,引用地址无法重新被赋值
//son2 = new Son();
}
//final能不能修饰局部变量?修饰基本类型和引用类型的区别
//final修饰的局部变量也是常量
//final修饰的引用类型变量,变量中的属性值是可以被改变的
//final修饰的引用的类型的变量,引用地址无法重新被赋值
}
7.18 接口
Java中的接口 :
概念 :接口就是一种规则,也是一种约束,实现类必须要遵守接口中定义的信息,并且还可以解决单继承的局限性
接口的特点:
写法 : 使用interface关键字,其实就是将类的class换成interface
public interface 接口名{}
子类实现接口:
class 类名 implements 接口名{ }
interface 子接口 extends 父接口{}
其他特点 :
1,接口不能被实例化
2,接口的实现类 ,
可以是一个具体类,具体类中,必须要实现父接口的全部抽象方法
可以是一个抽象类,抽象类可以不用实现全部抽象方法
3,成员特点:
成员变量
成员变量只能是常量,默认的修饰符是 : public static final
构造方法
接口中没有构造方法
成员方法
默认只能存在抽象方法,默认修饰符就是 public abstract
jdk8针对接口的不容易扩展现象,增加了static 和 default方法,如果定义这两个方法,方法中必须要写方法体用来实现某些功能,主要用来增加接口的灵活性
jdk9之后,在接口中又增加了私有private修饰的方法和private修饰的静态方法,这些私有的方法,主要可以改善接口内部代码的可重用性
public interface Person {
void say();
}
public interface Father extends Person {
//接口中的成员变量只能是常量,即使变量之前没有加final,仍然默认会去添加final
String name = "jack";
//public Father();
//接口中可以有抽象方法
public abstract void playFootBall();
//普通方法,接口中不能有普通方法
// public void method(){
//
// }
//静态方法,接口中可以有静态方法
public static void method01(){
}
//默认方法,接口中可以存在默认方法,使用default修饰的
default void method02(){
}
}
public interface Mother extends Person {
void sing();
}
public class Son implements Father,Mother {
@Override
public void playFootBall() {
System.out.println("儿子会踢球");
}
@Override
public void sing() {
System.out.println("儿子会唱歌");
}
@Override
public void say() {
}
}
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.playFootBall();
son.sing();
//son.name = "tom"; //name是用final修饰的,是常量
System.out.println(son.name);
//new Father(); //接口不能被实例化
}
}
设计一个简单的USB接口:
//usb接口
public interface USBInterface {
void service();
}
/*
U盘类
*/
public class UDisk implements USBInterface {
@Override
public void service() {
System.out.println("连接USB,U盘开始工作!");
}
}
//鼠标类
public class UMouse implements USBInterface {
@Override
public void service() {
System.out.println("鼠标连接上USB接口,开始工作!");
}
}
public class Test {
public static void main(String[] args) {
//多态创建接口对象
USBInterface usb = new UDisk();
usb.service();
USBInterface usb1 = new UMouse();
usb1.service();
}
}
抽象类和接口的比较:
7.19 内部类
概念 : 把类定义在其他类的内部,这个类就被称为内部类
特点 : 内部类可以直接访问外部类的成员,包括私有的
外部类要访问内部类的成员,需要创建对象
分类 :
定义在成员位置 :称为成员内部类
定义在局部位置:一般指的是定义在外部类类的方法中,称为局部内部类
public class Outer {
private int num = 100; //外部类的私有属性
//成员内部类
class Inner{
}
//外部类的成员方法
public void method(){
//局部内部类
class Inner{
}
}
}
7.19.1 成员内部类:
普通成员内部类
public class Outer {
private int num = 100; //外部类的私有属性
private static int num1 = 200;
//成员内部类
class Inner{
//内部类可以使用的修饰符
//private static
//内部类的普通方法
public void show(){
//内部类加了static之后,只能访问外部类中加了static的属性
System.out.println(num);
System.out.println(num1);
}
//如果内部类没有static,内部不能定义static方法
//public static void show2(){
// //System.out.println(num);
// System.out.println(num1);
//}
}
}
public class InnerDemo {
public static void main(String[] args) {
//创建内部类的对象的写法 :
//外部类名.内部类名 对象名 = 外部类对象.内部类对象
Outer.Inner inner = new Outer().new Inner();
inner.show();
}
}
静态成员内部类
public class Outer {
private int num = 100; //外部类的私有属性
private static int num1 = 200;
//成员内部类
static class Inner{
//内部类可以使用的修饰符
//private static
//内部类的普通方法
public void show(){
//内部类加了static之后,只能访问外部类中加了static的属性
//System.out.println(num);
System.out.println(num1);
}
//如果内部类没有static,内部不能定义static方法
public static void show2(){
//System.out.println(num);
System.out.println(num1);
}
}
}
public class InnerDemo {
public static void main(String[] args) {
//静态的内部类对象创建方式:
//外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner inner1 = new Outer.Inner();
inner1.show();
inner1.show2();
//静态内部类中的静态方法,可以通过类名直接访问
Outer.Inner.show2();
//Outer.Inner.show();
}
}
7.19.2 局部内部类:
1,可以直接访问外部类的成员
2,可以在局部位置,创建内部类的对象,通过对象调用内部类的方法,从而来使用局部内部类的功能
public class Outer {
private String str = "hello";
public void method(String a){
//a = "world"; //变量a重新赋值会报错
//局部内部类
class Inner{
//内部类的成员方法
public void show(){
System.out.println(str); //直接访问外部类的成员属性
System.out.println(a); //访问局部变量
}
}
//在局部位置,直接创建内部类的对象
Inner inner = new Inner();
inner.show();
}
}
public class InnerTest {
public static void main(String[] args) {
//创建外部类对象,通过外部类对象直接调用成员方法
//从而调用到成员方法中的局部内部类
Outer outer = new Outer();
outer.method("你好");
}
}
局部内部类访问局部变量的时候的问题
1,局部内部类访问的局部变量,必须是用final修饰的
为什么?
局部变量是随着方法的调用而存在,方法调用完毕而消失
局部内部类对象是在堆中,堆中的内容不会马上消失,所以如果不加final修饰,可能会出现一个没有消失的对象,在调用已经消失的局部变量,这种情况是不合理的。
加了final修饰后,变量就变为常量了,消失之后内存中的数据还是存在
jdk8之后,局部内部类的局部变量,即使没有加上final,系统也会默认变量是final的
7.19.2.1 匿名内部类
匿名内部类是内部类的简化写法
需要前提 :需要存在一个类或者接口,这个类可以是具体类,也可以是抽象类
new 类名或者接口名(){
重写方法;
}
public interface Inner {
void show1();
void show2();
}
public class Outer {
//外部类的成员方法
public void method(){
//这个操作,相当于在创建接口的实现类
//这个实现类,没有名字,称为匿名内部类
//相当于把实现Inner接口的实现类,和实现类的对象都创建了
//完成了4个动作 :
// 1,Inner接口实现类的创建 2,接口方法的重写 3,实现类对象的创建 4,重写方法的调用
/* new Inner(){
//实现接口中的方法
@Override
public void show1() {
System.out.println("匿名内部类重写的show1方法~");
}
@Override
public void show2() {
System.out.println("匿名内部类重写的show2方法~");
}
}.show1();
//接口中有多个方法的话,写实现就得写多次,比较麻烦
new Inner(){
//实现接口中的方法
@Override
public void show1() {
System.out.println("匿名内部类重写的show1方法~");
}
@Override
public void show2() {
System.out.println("匿名内部类重写的show2方法~");
}
}.show2();
*/
//使用多态来改进写法,父接口的引用指向子类的对象
Inner inner = new Inner(){
//实现接口中的方法
@Override
public void show1() {
System.out.println("匿名内部类重写的show1方法~");
}
@Override
public void show2() {
System.out.println("匿名内部类重写的show2方法~");
}
};
inner.show1();
inner.show2();
}
}
public class OuterTest {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}
匿名内部类的笔试题:
public interface Inner {
void show();
}
public class Outer {
//等待补齐代码
}
public class OuterTest {
public static void main(String[] args) {
Outer.method().show();
//在后台输出 :"HelloWord!"
}
}
补充代码:
//Outer.method()
//说明存在静态的method()方法
//Outer.method().show() 说明method方法应该返回Inner对象
public static Inner method(){
return new Inner() {
@Override
public void show() {
System.out.println("HelloWorld!");
}
};
}
7.20 设计模式
通过设计模式的完成计算器的使用:
基本代码实现
/*
基本代码实现计算器
*/
public class Demo01 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入计算的第一个数字:");
int a = scanner.nextInt();
System.out.println("请输入运算符号:");
String b = scanner.next();
System.out.println("请输入计算的第二个数字:");
int c = scanner.nextInt();
int result = 0;
switch (b){
case "+":
result = a + c;
break;
case "-":
result = a - c;
break;
case "*":
result = a * c;
break;
case "/":
result = a / c;
break;
default:
System.out.println("运算符输入错误!");
return;
}
System.out.println( a + b + c + "=" +result);
}
}
面向对象实现 :
//面向对象实现
public class Operation {
public static int getResult(int a,int b,String operate){
int result = 0;
switch (operate){
case "+":
result = a + b;
break;
case "-":
result = a - b;
break;
case "*":
result = a * b;
break;
case "/":
result = a / b;
break;
default:
System.out.println("运算符输入错误!");
break;
}
return result;
}
}
public class Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入计算的第一个数字:");
int a = scanner.nextInt();
System.out.println("请输入运算符号:");
String b = scanner.next();
System.out.println("请输入计算的第二个数字:");
int c = scanner.nextInt();
int result = Operation.getResult(a, c, b);
System.out.println(result);
}
}
利用设计模式的工厂方法模式:
//计算类抽象类
public abstract class Operation {
private double num1;
private double num2;
public abstract double getResult();
public double getNum1() {
return num1;
}
public void setNum1(double num1) {
this.num1 = num1;
}
public double getNum2() {
return num2;
}
public void setNum2(double num2) {
this.num2 = num2;
}
}
//加法类
public class Add extends Operation {
@Override
public double getResult() {
return getNum1() + getNum2();
}
}
//减法类
public class Minus extends Operation {
@Override
public double getResult() {
return getNum1() - getNum2();
}
}
//运算工厂类
public class OperationFactory {
public static Operation createOperation(String operate){
if (operate.equals("+")){
return new Add();
}else if (operate.equals("-")){
return new Minus();
}
return null;
}
}
//测试类
public class Test {
public static void main(String[] args) {
Operation o = OperationFactory.createOperation("+");
o.setNum1(10);
o.setNum2(20);
System.out.println(o.getResult());
}
}
利用设计模式的抽象工厂模式:
//计算类抽象类
public abstract class Operation {
private double num1;
private double num2;
public abstract double getResult();
public double getNum1() {
return num1;
}
public void setNum1(double num1) {
this.num1 = num1;
}
public double getNum2() {
return num2;
}
public void setNum2(double num2) {
this.num2 = num2;
}
}
//加法类
public class Add extends Operation {
@Override
public double getResult() {
return getNum1() + getNum2();
}
}
//减法类
public class Minus extends Operation {
@Override
public double getResult() {
return getNum1() - getNum2();
}
}
//计算工厂抽象类
public abstract class OperationFactory {
public abstract Operation createOperation();
}
//加法工厂
public class AddFactory extends OperationFactory {
@Override
public Operation createOperation() {
return new Add();
}
}
//减法工厂
public class MinusFactory extends OperationFactory {
@Override
public Operation createOperation() {
return new Minus();
}
}
//测试类
public class Test {
public static void main(String[] args) {
AddFactory addFactory = new AddFactory();
Operation operation = addFactory.createOperation();
operation.setNum1(10);
operation.setNum2(20);
System.out.println(operation.getResult());
}
}
使用抽象工厂之后,添加一个运算,只要在写子类继承即可:
比如,加个乘法类:
//乘法类
public class ChengFa extends Operation {
@Override
public double getResult() {
return getNum1() * getNum2();
}
}
//乘法工厂
public class ChengFaFactory extends OperationFactory {
@Override
public Operation createOperation() {
return new ChengFa();
}
}
//测试类
ChengFaFactory chengFaFactory = new ChengFaFactory();
Operation operation1 = chengFaFactory.createOperation();
operation1.setNum2(10);
operation1.setNum1(20);
System.out.println(operation1.getResult());
8 面向对象、设计思想
面向对象编程思想
简介
从面向对象编程思想是如何解决软件开发中各种疑难问题的角度,讲述面向对象编程思想的理解,梳理面向对象四大基本特性、七大设计原则和23种设计模式之间的关系
8.1 软件开发中疑难问题:
软件复杂庞大;
很多软件很难维护;
需求不断变更;
软件开发中存在很多很多的问题,上面三个问题只是从程序开发和设计的角度看到的部分问题。要解决上面软件开发的问题,就要求我们编写(设计)的软件具有很好的可读性、可维护性和可扩展性,并且还要保证代码具有高内聚低耦合。
8.2 四大基本特性
OOP(面向对象编程)的四大特性:抽象、封装、多态、继承。
8.2.1 抽象:
抽象是提取现实世界中某事物的关键特性,为该事物构建模型的过程。对同一事物中不同的需求下,需要提取的特性可能不一样。得到的抽象模型中一般包含:属性(数据)和操作(行为)。这个抽象模型我们称之为类,通过对类进行实例化得到具体的对象。
8.2.2 封装:
封装可以使类具有独立性和隔离性,从而保证类的高内聚。只暴露给类外部或者子类必须的属性和操作。类封装的实现依赖类的修饰符(public、protected和private)。
8.2.3 继承:
继承是对现有类的一种复用机制。一个类如果继承现有的类,则这个类将拥有被继承类的所有非私有特性(属性和操作)。这里指的继承包含类的继承和接口的实现。常见的继承方法有共有继承、私有继承、多重继承、多层继承。
8.2.4 多态:
多态是中继承的基础上实现的。多态的三个要素:继承、重写和父类引用指向子类的对象。父类引用指向不同的子类对象时,调用相同的方法,呈现出不同的行为;就是类的多态性。多态还可分为编译时多态(静态多态性)和运行时多态(动态多态性)。另外类的函数重载、运算符重载,也可算为多态的特性。
8.2.5 内聚:
故名思议,表示内部间聚集、关联的程度,那么高内聚就是指要高度的聚集和关联。
高内聚是指类与类之间的关系而定,高,意思是他们之间的关系要简单,明了,不要有很强的关系,不然,运行起来就会出问题。一个类的运行影响到其他的类。
由于高内聚具备可靠性,可重用性,可读性等优点,模块设计推荐采用高内聚。内聚标志一个模块内各个元素彼此结合的紧密程度,它是信息隐蔽和局部化概念的自然扩展。
内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。
8.2.6 耦合:
是对模块间关联程度的度量。
耦合的强弱取决与模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。
模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。
模块间联系越多,其耦合性越强,同时表明其独立性越差。降低模块间的耦合度能减少模块间的影响,防止对某一模块修改所引起的“牵一发动全身”的水波效应,保证系统设计顺利进行。 耦合度就是某模块(类)与其它模块(类)之间的关联
8.3 七大设计原则
在面向对象四大基础特性之上,我们在做面向对象设计时,还需要遵循一些基本设计原则。
、感知和依赖的程度,是衡量代码独立性的一个指标。
一、设计模式的七大原则
8.3.1、单一职责原则(SRP)
对类来说,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以要将类A的粒度分解为A1,A2。
8.3.2、开闭原则(OCP)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热拔插的效果,所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
在面向对象的编程中,开闭原则是最基础的原则,起到总的指导作用,其他原则(单一职责、里氏替换、依赖倒置等)都是开闭原则的具体形态,即其他原则都是开闭原则的手段和工具。
举个简单例子,这里有个生产电脑的公司,根据输入的类型,生产出不同的电脑,代码如下:
interface Computer {}
class Macbook implements Computer {}
class Surface implements Computer {}
class Factory {
public Computer produceComputer(String type) {
Computer c = null;
if(type.equals("macbook")){
c = new Macbook();
}else if(type.equals("surface")){
c = new Surface();
}
return c;
}
}
显然上面的代码违背了开放 - 关闭原则,如果需要添加新的电脑产品,那么修改 produceComputer 原本已有的方法,正确的方式如下:
interface Computer {}
class Macbook implements Computer {}
class Surface implements Computer {}
interface Factory {
public Computer produceComputer();
}
class AppleFactory implements Factory {
public Computer produceComputer() {
return new Macbook();
}
}
class MSFactory implements Factory {
public Computer produceComputer() {
return new Surface();
}
}
8.3.3、里氏代换原则(LSP)
在面向对象的语言中,继承是必不可少的,它主要有以下几个优点:
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
- 提高代码的可重用性;
- 提高代码的可扩展性;
- 提高产品或项目的开放性。
相应的,继承也存在缺点,主要体现在以下几个方面:
- 继承是入侵式的。只要继承,就必须拥有父类的所有属性和方法;
- 降低代码的灵活性。子类必须拥有父类的属性和方法,使子类受到限制;
- 增强了耦合性。当父类的常量、变量和方法修改时,必须考虑子类的修改,这种修改可能造成大片的代码需要重构。
里氏代换原则中说: 所有引用父类的地方必须能透明地使用其子类对象。清晰明确地说明只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道父类还是子类;但是反过来则不可以,有子类的地方,父类未必就能适应。
LSP是继承复用的基石,只有衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类已经实现功能的方法。
示例:
我们需要完成一个两数相减的功能
class A{
public int func1(int a, int b)
{return a-b;}
}
public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50="+a.func1(100, 50));
System.out.println("100-80="+a.func1(100, 80));
}
}
运行结果:
100-50=50
100-80=20
后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:
- 两数相减
- 两数相加,然后再加100
class B extends A{
public int func1(int a, int b){
return a+b;
}
public int func2(int a, int b){
return func1(a,b)+100;
}
}
public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50="+b.func1(100, 50));
System.out.println("100-80="+b.func1(100, 80));
System.out.println("100+20+100="+b.func2(100, 20));}
}
8.3.4、依赖倒转原则(DIP)
开闭原则的基础,具体内容:针对接口编程,不要对实现编程;
高层模块不应该依赖低层模块。两个都应该依赖抽象。
抽象不应该依赖细节。细节应该依赖抽象。
例子:以顾客购物场景为例,假设今天,顾客想去万达买点东西,就有了下面代码
class Customer {
public void shopping(WanDaShop shop) {
//购物
System.out.println(shop.sell());
}
}
第二天,顾客觉得万达东西太贵了,想换一家店购买,比如永辉超市,于是又修改代码为:
class Customer {
public void shopping(YonghuiShop shop) {
//购物
System.out.println(shop.sell());
}
}
此时,缺点已经体现出来了,顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。
解决方法是:定义“永辉超市”和“万达的”的共同接口 Shop,顾客类面向该接口编程,其代码修改如下:
class Customer {
public void shopping(Shop shop) {
//购物
System.out.println(shop.sell());
}
}
这样,不管顾客类 Customer 访问什么商店,或者增加新的商店,都不需要修改原有代码了。
8.3.5、接口隔离原则(ISP)
使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便.(降低依赖,降低耦合)。
根据接口隔离原则,当一个接口太大的时候,我们就需要将它分割为一些更加细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
例子:以学生成绩管理系统为例,学生成绩管理一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能,如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中,其类图如图所示。
8.3.6、迪米特法则(LoD)
迪米特法则也称最少知道原则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
根本思想:强调了类之间的松耦合。
示例:以明星和经纪人关系为例,明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则,其类图如图所示
public class LoDtest {
public static void main(String[] args) {
Agent agent = new Agent();
agent.setStar(new Star("周杰伦"));
agent.setFans(new Fans("粉丝某某"));
agent.setCompany(new Company("中国传媒有限公司"));
agent.meeting();
agent.business();
}
}
//经纪人
class Agent {
private Star myStar;
private Fans myFans;
private Company myCompany;
public void setStar(Star myStar) {
this.myStar = myStar;
}
public void setFans(Fans myFans) {
this.myFans = myFans;
}
public void setCompany(Company myCompany) {
this.myCompany = myCompany;
}
public void meeting() {
System.out.println(myFans.getName() + "与明星" + myStar.getName() + "见面了。");
}
public void business() {
System.out.println(myCompany.getName() + "与明星" + myStar.getName() + "洽淡业务。");
}
}
//明星
class Star {
private String name;
Star(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//粉丝
class Fans {
private String name;
Fans(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//媒体公司
class Company {
private String name;
Company(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
8.3.7、合成复用原则(CRP)
合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。
它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
示例:
以汽车分类管理系统为例,汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。
我们先看用继承关系实现的汽车分类的类图:
从上图可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。
但如果改用组合关系实现就能很好地解决以上问题:
8.4 设计模式的分类:
8.4.1、单例模式(Singleton):
保证一个类只有一个实例,并提供一个访问它的全局访问点。
public class Person {
public static final Person person = new Person();
//构造方法私有化
private Person(){}
public static Person getInstance(){
return person;
}
}
public class Test {
public static void main(String[] args) {
//Person person = new Person();
//Person person1 = new Person();
//System.out.println(person == person1);
Person p1 = Person.getInstance();
Person p2 = Person.getInstance();
System.out.println(p1 == p2);
}
}
8.4.2、抽象工厂(Abstract Factory):
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
介绍
意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
主要解决:主要解决接口选择的问题。
何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
如何解决:在一个产品族里面,定义多个产品。
关键代码:在一个工厂里聚合多个同类产品。
应用实例:工作了,为了参加一些聚会,肯定有两套或多套衣服吧,比如说有商务装(成套,一系列具体产品)、时尚装(成套,一系列具体产品),甚至对于一个家庭来说,可能有商务女装、商务男装、时尚女装、时尚男装,这些也都是成套的,即一系列具体产品。假设一种情况(现实中是不存在的,要不然,没法进入共产主义了,但有利于说明抽象工厂模式),在您的家中,某一个衣柜(具体工厂)只能存放某一种这样的衣服(成套,一系列具体产品),每次拿这种成套的衣服时也自然要从这个衣柜中取出了。用 OOP 的思想去理解,所有的衣柜(具体工厂)都是衣柜类的(抽象工厂)某一个,而每一件成套的衣服又包括具体的上衣(某一具体产品),裤子(某一具体产品),这些具体的上衣其实也都是上衣(抽象产品),具体的裤子也都是裤子(另一个抽象产品)。
优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。
使用场景: 1、QQ 换皮肤,一整套一起换。 2、生成不同操作系统的程序。
注意事项:产品族难扩展,产品等级易扩展。
示例
我们将创建 Shape 和 Color 接口和实现这些接口的实体类。
下一步是创建抽象工厂类 AbstractFactory。接着定义工厂类 ShapeFactory 和 ColorFactory,这两个工厂类都是扩展了 AbstractFactory。然后创建一个工厂创造器/生成器类 FactoryProducer。
AbstractFactoryPatternDemo,我们的演示类使用 FactoryProducer 来获取 AbstractFactory 对象。
它将向 AbstractFactory 传递形状信息 Shape(CIRCLE / RECTANGLE / SQUARE),以便获取它所需对象的类型。同时它还向 AbstractFactory 传递颜色信息 Color(RED / GREEN / BLUE),以便获取它所需对象的类型。
步骤 1
为形状创建一个接口。
public interface Shape {
void draw();
}
步骤2
创建实现接口的实体类。
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("画长方形");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("画正方形");
}
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆形");
}
}
步骤3
为颜色创建一个接口。
public interface Color {
void fill();
}
步骤4
创建实现接口的实体类。
public class Red implements Color {
@Override
public void fill() {
System.out.println("红色");
}
}
public class Green implements Color {
@Override
public void fill() {
System.out.println("绿色");
}
}
public class Blue implements Color {
@Override
public void fill() {
System.out.println("蓝色");
}
}
步骤5
为 Color 和 Shape 对象创建抽象类来获取工厂。
public abstract class AbstractFactory {
public abstract Color getColor(String color);
public abstract Shape getShape(String shape) ;
}
步骤6
创建扩展了 AbstractFactory 的工厂类,基于给定的信息生成实体类的对象。
public class ShapeFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("圆形")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("长方形")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("正方形")){
return new Square();
}
return null;
}
@Override
public Color getColor(String color) {
return null;
}
}
public class ColorFactory extends AbstractFactory {
@Override
public Shape getShape(String shapeType){
return null;
}
@Override
public Color getColor(String color) {
if(color == null){
return null;
}
if(color.equalsIgnoreCase("红色")){
return new Red();
} else if(color.equalsIgnoreCase("绿色")){
return new Green();
} else if(color.equalsIgnoreCase("蓝色")){
return new Blue();
}
return null;
}
}
步骤 7 创建一个工厂创造器/生成器类,通过传递形状或颜色信息来获取工厂。
public class FactoryProducer {
public static AbstractFactory getFactory(String choice){
if(choice.equalsIgnoreCase("图形")){
return new ShapeFactory();
} else if(choice.equalsIgnoreCase("颜色")){
return new ColorFactory();
}
return null;
}
}
步骤 8 使用 FactoryProducer 来获取 AbstractFactory,通过传递类型信息来获取实体类的对象。
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
//获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("图形");
//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("圆形");
//调用 Circle 的 draw 方法
shape1.draw();
//获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("长方形");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取形状为 Square 的对象
Shape shape3 = shapeFactory.getShape("正方形");
//调用 Square 的 draw 方法
shape3.draw();
//获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("颜色");
//获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("红色");
//调用 Red 的 fill 方法
color1.fill();
//获取颜色为 Green 的对象
Color color2 = colorFactory.getColor("绿色");
//调用 Green 的 fill 方法
color2.fill();
//获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("蓝色");
//调用 Blue 的 fill 方法
color3.fill();
}
}
步骤 9 执行程序,输出结果:
8.4.3、工厂方法(Factory Method):
定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
介绍
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
主要解决:主要解决接口选择的问题。
何时使用:我们明确地计划不同条件下创建不同实例时。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
关键代码:创建过程在其子类执行。
应用实例: 1、您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。 2、Hibernate 换数据库只需换方言和驱动就可以。
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
使用场景: 1、日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。 2、数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。 3、设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。
注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
示例:
我们将创建一个 Shape 接口和实现 Shape 接口的实体类。
下一步是定义工厂类 ShapeFactory。
FactoryPatternDemo,我们的演示类使用 ShapeFactory 来获取 Shape 对象。
它将向 ShapeFactory 传递信息(CIRCLE / RECTANGLE / SQUARE),以便获取它所需对象的类型。
步骤 1
创建一个接口:
Shape.java
public interface Shape { void draw(); }
步骤 2
创建实现接口的实体类。
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("画长方形");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("画正方形");
}
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆形");
}
}
步骤3
创建一个工厂,生成基于给定信息的实体类的对象。
public class ShapeFactory {
//使用 getShape 方法获取形状类型的对象
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("圆形")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("长方形")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("正方形")){
return new Square();
}
return null;
}
}
步骤 4
使用该工厂,通过传递类型信息来获取实体类的对象。
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
//获取 Circle 的对象,并调用它的 draw 方法
Shape shape1 = shapeFactory.getShape("圆形");
//调用 Circle 的 draw 方法
shape1.draw();
//获取 Rectangle 的对象,并调用它的 draw 方法
Shape shape2 = shapeFactory.getShape("长方形");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取 Square 的对象,并调用它的 draw 方法
Shape shape3 = shapeFactory.getShape("正方形");
//调用 Square 的 draw 方法
shape3.draw();
}
}
4、建造模式(Builder):讲一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
5、原型模式(Peototype):用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
行为型:
6、迭代器模式(Iterator):提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
7、观察者模式(Observer):定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
8、模板方法(Temolate Method):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。
9、命令模式(Command):讲一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
10、状态模式(State):允许对象在其内部状态改变时改变他的行为。
11、策略模式(Strategy):定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。
12、职责链模式(ChinaOfResponibility):使多个对象都有机会请求处理请求,从而避免请求的发送者和接收者之间的耦合关系。
13、中介者模式(Mediator):用一个中介对象封装一些列的对象交互。
14、访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它可以在不改变各元素类的前提下定义作用域这个元素的新操作。
15、解释器模式(Interpreter):给定一个语言,定义它的文法的一个表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
16、备忘录模式(Memento):在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
结构型:
17、组合模式(Composite):将对象组合树形结构以表示部分整体的关系。
18、外观模式(Facade):为子系统中的一组接口提供一致的界面,façade提供了一高层接口。这个接口使得子系统更容易使用。
19、代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
20、适配器模式(Adapter):将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以在一起作用。
21、装饰模式(Decrator):动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生产子类更加灵活。
22、桥接模式(Biedge):将抽象部分与它的实现部分相分离,使他们可以独立的变化。
23、享元模式(Flyweight):运用共享技术有效地支持大量细粒度的对象。
9 常用类
9.1 Object类
Object是Java中,类结构的根类,所有的类都是直接或者间接继承Object类
Object类有一个默认的无参构造方法,用来给子类访问
9.1.1 Object类中的方法
需要掌握:
1,boolean equals(Object obj) 指示一些其他对象是否等于此。
2,int hashCode() 返回对象的哈希码值。
3,String toString() 返回对象的字符串表示形式。
需要了解:
4,protected Object clone() 创建并返回此对象的副本。
5,protected void finalize() 当垃圾收集确定不再有对该对象的引用时,垃圾收集器在对象上调用该对象 ,对象被回收的时候,可能会调用这个方法
6,Class<?> getClass() 返回此 对象的 字节码文件对象。
public class Student implements Cloneable {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TestStudent {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student("张三", 20);
//调用克隆方法
Object o = s1.clone();
Student s2 = (Student)o;
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}
9.2 System类
System类包含几个有用的类字段和方法。 它不能被实例化。
System类提供的System包括标准输入,标准输出和错误输出流;
访问外部定义的属性和环境变量; 一种加载文件和库的方法; 以及用于快速复制数组的一部分的实用方法。
常用方法:
static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 将指定源数组中的数组从指定位置复制到目标数组的指定位置。
int[] arr = {10,20,30,40,50};
int[] arr1 = {100,200,300,400,500};
//当前数组复制指定数组中的内容,替换当前数组指定位置的内容
System.arraycopy(arr,2,arr1,3,2);
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arr1));
static long currentTimeMillis() 返回当前时间(以毫秒为单位)。
//当前时间的毫秒值,一般用来在某段代码执行的前后分别返回,用来计算程序的执行时间
System.out.println(System.currentTimeMillis());
static void exit(int status) 终止当前运行的Java虚拟机。
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5){
//表示终止当前正在运行的Java虚拟机,也就是结束程序
//一般,非0的状态码表示异常终止
System.exit(0);
}
}
static void gc() 运行垃圾回收器。
结合finalize()方法,观察gc()方法对于垃圾对象的回收,只有没有被引用的对象才会被gc()方法明确的回收
public class Student implements Cloneable {
@Override
protected void finalize() throws Throwable {
System.out.println("对象被回收了,finalize()方法自动被调用了!");
}
}
public class TestStudent {
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
new Student();
new Student();
new Student();
System.gc();
}
}
9.3 包装类(会用)
概念: 基本数据类型,没有方法可以使用,将来如果需要完成基本数据类型的转换,不太方便,所以系统就提供了 包装类,包装类其实就是将基本类型,以对象的形式表示出来,提供了一些方法,可以调用,从而是基本类型间接的使用对象的方法。
将来Java集合中,只能存放对象,所以想要在集合中存入基本数据类型,必须要以包装类的形式存放
基本数据类型对应的包装类 :
byte => Byte
short => Short
int =>Integer
long =>long
float =>Float
Float(float value) 构造一个新分配的 Float对象,该对象表示基元 float参数。
Float(String s) 构造一个新分配 Float对象,它表示类型的浮点值 float用字符串表示。
double => Double
char => Character
Character(char value) 构造一个新分配的 Character对象,代表指定的 char值。
boolean =>Boolean
包装类的特点 :
1,构造方法 : 除了Character类以外,其他的包装类,都可以以它基本类型的数据或者一个字符串作为参数,来构造对应的包装类对象
2,普通方法:包装类通用的一些方法
//1,XXXValue:将包装类转为基本类型
//2,toString() 将包装类以字符串的形式返回
//3,valueOf() 包装类都有一个valueOf方法,通过类名直接调用,传入字符串或者基本类型,返回包装类型
注意,传入字符串的时候,不能随便传,要符合对应的类型
//4,parseXXX : 将字符串转换为对应包装类的基本数据类型
public class Demo01 {
public static void main(String[] args) {
Integer i1 = new Integer(123);
//1,XXXValue:将包装类转为基本类型
int i2 = i1.intValue();
//2,toString() 将包装类以字符串的形式返回
String i3 = i1.toString();
//3,valueOf() 包装类都有一个valueOf方法
//通过类名直接调用,传入字符串或者基本类型,返回包装类型
//注意,传入字符串的时候,不能随便传,要符合对应的类型
Integer i4 = Integer.valueOf("100");
//4,parseXXX : 将字符串转换为对应包装类的基本数据类型
int i5 = Integer.parseInt("100");
Float f1 = new Float(3.3);
//1,XXXValue:将包装类转为基本类型
float f2 = f1.floatValue();
Boolean b1 = new Boolean(true);
//1,XXXValue:将包装类转为基本类型
boolean b2 = b1.booleanValue();
Character c1 = new Character('A');
//1,XXXValue:将包装类转为基本类型
char c2 = c1.charValue();
//Character类的valueof没有字符串类型的参数
Character.valueOf('a');
}
}
jdk1.5新特性 : 自动装箱和自动拆箱
//创建包装类对象
//Integer i = new Integer(100);
//自动装箱,将基本类型的值,直接赋值给对应的包装类对象
//系统其实省略一个步骤:
//Integer.valueOf(100);
Integer i = 100;
Character c = 'a';
//自动拆箱,将包装类型的对象,直接赋值给对应基本类型变量
//中间省略调用intValue()方法这个步骤
//int i1 = i.intValue();
int a = i; //直接把包装类对象,赋值给基本类型的变量
9.4 Integer类型
构造方法:
Integer(int value) 构造一个新分配的 Integer对象,该对象表示指定的 int值。
Integer(String s) 构造一个新分配 Integer对象,表示 int由指示值 String参数
普通方法:
static String toBinaryString(int i) 在基数2中返回整数参数的字符串表示形式为无符号整数。
static String toHexString(int i) 返回整数参数的字符串表示形式,作为16位中的无符号整数。
static String toOctalString(int i) 在基数8中返回整数参数的字符串表示形式为无符号整数。
9.4.1 String和int类型的转换:
// int -> String
int num = 100;
//方式1:
String s1 = num + "";
//方式2:
String s2 = String.valueOf(num);
//方式3: int -> Integer -> String
Integer i = new Integer(num);
String s3 = i.toString();
//方式4:
String s4 = Integer.toString(num);
//String->int
String str = "200";
//方式1:
int i1 = Integer.parseInt(str);
//方式2:
Integer integer = new Integer(str);
int i2 = integer.intValue();
9.4.2 Integer类的缓冲区
//如果Integer对象的值,以值形式直接赋值的话
//值在-128~127 的范围内,数值会从 缓冲区中直接拿取,
// 判断对象是否相等,返回true
//超过这个返回,判断相等返回false
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); //true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); //false
9.5 Character类
Character类包装一个对象中的基本类型char的值。 类型为Character的对象包含一个单一字段,其类型为char 。
此外,该类还提供了几种方法来确定字符的类别(小写字母,数字等),并将字符从大写转换为小写,反之亦然。
构造方法:
Character(char value) 构造一个新分配的 Character对象,代表指定的 char值。
普通方法:
static boolean isDigit(char ch) 确定指定的字符是否是数字。
static boolean isLetter(char ch) 确定指定的字符是否是一个字母。
static boolean isLowerCase(char ch) 确定指定的字符是否是小写字符。
static boolean isUpperCase(char ch) 确定指定的字符是否为大写字符。
static String toString(char c) 返回一个 String对象,表示指定的 char 。
static char toLowerCase(char ch) 使用UnicodeData文件中的大小写映射信息将字符参数转换为小写。
static char toUpperCase(char ch) 使用UnicodeData文件中的案例映射信息将字符参数转换为大写
/*
测试Character类的常用方法
*/
public class Demo05 {
public static void main(String[] args) {
//char c = 'a';
//System.out.println(Character.isDigit(c));// false
//System.out.println(Character.isLowerCase(c));// true
//System.out.println(Character.isUpperCase(c));// false
//System.out.println(Character.isLetter(c));//true
//System.out.println(Character.toUpperCase(c));//A
//System.out.println(Character.toLowerCase(c));//a
//让用户输入一段字符串数字加大小写字母混合,比如:asgsa2412fafGVSD312dfa
//这段字符串中,有几个数字、几个小写字母、几个大写字母
//1,创建输入对象,输入字符串,声明字符串
String s = "asgsa2412fafGVSD312dfa";
//2,将字符串转成字符数组
char[] chars = s.toCharArray();
//3,遍历数组
//声明统计变量,三个
int a = 0, b = 0, c = 0;
for (int i = 0; i < chars.length; i++) {
//3.1判断每个字符是什么,数字、小写字母、大写字母
char ch = chars[i];
//3.2判断之后给对应的统计变量做++
if (Character.isDigit(ch)) { //如果是数字
a++;
}
if (Character.isLowerCase(ch)){ //如果是小写字母
b++;
}
if (Character.isUpperCase(ch)){ //如果是大写字母
c++;
}
}
//4,输出统计变量
System.out.println("数字:" + a + ",小写字母:" + b + ",大写字母:" +c);
}
}
9.6 其他类
9.6.1 Math类
Math类包含执行基本数字运算的方法,如基本指数,对数,平方根和三角函数。
常用方法:
static double ceil(double a) 返回大于或等于参数的最小(最接近负无穷大) double值,等于一个数学整数。
static double floor(double a) 返回小于或等于参数的最大(最接近正无穷大) double值,等于一个数学整数
static int max(int a, int b) 返回两个 int值中的较大值。
static int min(int a, int b) 返回两个 int的较小值。
static double random() 返回值为 double值为正号,大于等于 0.0 ,小于 1.0 。
static long round(double a) 返回参数中最接近的 long ,其中 long四舍五入为正无穷大。
//Math演示
public class Demo06 {
public static void main(String[] args) {
System.out.println(Math.ceil(3.25)); // 4
System.out.println(Math.floor(3.25)); //3
System.out.println(Math.max(10,10.2)); //10.2
System.out.println(Math.min(10,5)); //5
System.out.println(Math.round(2.58));//3
System.out.println(Math.random());
//通过random()方法得到1-10之间的随机整数
System.out.println(Math.ceil(Math.random() * 10));
//11-20之间随机整数
System.out.println(Math.ceil(Math.random() * 10 + 10));
}
}
9.6.2 Random
生成随机数的类
构造方法:
Random() 创建一个新的随机数生成器。
Random(long seed) 使用单个 long种子创建一个新的随机数生成器。
普通方法:
int nextInt() 返回下一个伪随机数,从这个随机数发生器的序列中均匀分布 int值。
int nextInt(int bound) 返回伪随机的,均匀分布 int值介于0(含)和指定值(不包括),从该随机数生成器的序列绘制。
protected int next(int bits) 生成下一个伪随机数。
long nextLong() 返回下一个伪,均匀分布 long从这个随机数生成器的序列值
//Random类
public class Demo07 {
public static void main(String[] args) {
//无参构造的random对象
Random random = new Random();
int i = random.nextInt();
System.out.println(i);
int i1 = random.nextInt(100);
System.out.println(i1);
Random random1 = new Random(2000);
int i2 = random1.nextInt();
System.out.println(i2);
int i3 = random1.nextInt(100);
System.out.println(i3);
}
}
练习:
//写一个猜数字的小游戏
//提示用户,1-100随机数已经生成,让用户输入数值猜生成随机数大小
//如果猜小了,返回你猜小了
//猜大了,返回你猜大了
//猜中了,返回你猜对了,太棒了
//超过10次为猜中,游戏技术,并返回你太菜了,10次都没猜中!告诉他正确答案
9.6.3 BigInteger
大整数的运算
/*
BigInteger类
*/
public class Demo08 {
public static void main(String[] args) {
//Integer i = 1234567;
//i = i * 54321;
//System.out.println(i);
//System.out.println(Integer.MAX_VALUE);
Integer i1 = new Integer("2147483647");
System.out.println(i1);
//Integer i2 = new Integer("2147483648");
//System.out.println(i2);
BigInteger bi = new BigInteger("2147483648");
System.out.println(bi);
//System.out.println(bi + 100); //不能直接使用加号
BigInteger bi2 = new BigInteger("100");
//BigInteger add(BigInteger val)
//返回值为 (this + val)
System.out.println(bi.add(bi2));
//BigInteger divide(BigInteger val)
//返回值为 (this / val) 。
System.out.println(bi.divide(bi2));
//BigInteger multiply(BigInteger val)
//返回值为 (this * val) 。
System.out.println(bi.multiply(bi2));
//BigInteger subtract(BigInteger val)
//返回值为 (this - val) 。
System.out.println(bi.subtract(bi2));
//int intValue()
//将此BigInteger转换为 int
//如果bigInteger超过了范围,转为int之后,值会发生改变
int i = bi.intValue();
System.out.println(i);
}
}
9.6.4 BigDecimal
主要用来做浮点数的运算,防止精度丢失
构造方法
BigDecimal(String val) 将BigDecimal的字符串表示 BigDecimal转换为 BigDecimal 。
BigDecimal(int val) 将 int成 BigDecimal 。
BigDecimal(double val) 将 double转换为 BigDecimal ,这是 double的二进制浮点值的精确十进制表示。
public class Demo09 {
public static void main(String[] args) {
// new BigDecimal();
System.out.println(1-0.01);
System.out.println(0.09 + 0.01);
System.out.println(1.015 * 100);
//创建BigDecimal对象
BigDecimal b1 = new BigDecimal("0.09");
BigDecimal b2 = new BigDecimal("0.01");
System.out.println(b1.add(b2));
System.out.println(b1.subtract(b2));
System.out.println(b1.multiply(b2));
System.out.println(b1.divide(b2));
}
}
9.6.5 String类
String类用来表示字符串,所有的字符串都可以看做是String类的实例(对象)
String类的字符串,其实底层就是char类型的数组
String类声明的字符串是不可变的,它们的值在创建后不能被更改,如果字符串值被改变,那么其实是字符串变量对应的引用改变
构造方法 :
String() 初始化新创建的 String对象,使其表示空字符序列。
String(byte[] bytes) 通过使用平台的默认字符集解码指定的字节数组来构造新的 String
String(char[] value) 分配一个新的 String ,以便它表示当前包含在字符数组参数中的字符序列。
String(String original) 初始化新创建的String对象,使其表示与参数相同的字符序列; 换句话说,新创建的字符串是参数字符串的副本。
String(StringBuffer buffer) 分配一个新的字符串,其中包含当前包含在字符串缓冲区参数中的字符序列。
String(StringBuilder builder) 分配一个新的字符串,其中包含当前包含在字符串构建器参数中的字符序列。
普通方法 :
char charAt(int index) 返回 char指定索引处的值。
String concat(String str) 将指定的字符串连接到该字符串的末尾。
boolean contains(CharSequence s) 当且仅当此字符串包含指定的char值序列时才返回true。
boolean equals(Object anObject) 将此字符串与指定对象进行比较。
boolean equalsIgnoreCase(String anotherString) 将此 String与其他 String比较,忽略大小写
int indexOf(String str) 返回指定子字符串第一次出现的字符串内的索引。
int indexOf(String str, int fromIndex) 返回指定子串的第一次出现的字符串中的索引,从指定的索引开始。
boolean isEmpty() 返回 true如果,且仅当 length()为 0 。
int lastIndexOf(String str) 返回指定子字符串最后一次出现的字符串中的索引。
int lastIndexOf(String str, int fromIndex) 返回指定子字符串的最后一次出现的字符串中的索引,从指定索引开始向后搜索。
int length() 返回此字符串的长度。
String replace(char oldChar, char newChar)
返回从替换所有出现的导致一个字符串 oldChar在此字符串 newChar 。
String replaceAll(String regex, String replacement)
用给定的替换替换与给定的 regular expression匹配的此字符串的每个子字符串。
String replaceFirst(String regex, String replacement)
用给定的替换替换与给定的 regular expression匹配的此字符串的第一个子字符串。
String[] split(String regex)
将此字符串分割为给定的 regular expression的匹配。
String substring(int beginIndex)
返回一个字符串,该字符串是此字符串的子字符串。
String substring(int beginIndex, int endIndex)
返回一个字符串,该字符串是此字符串的子字符串。
char[] toCharArray()
将此字符串转换为新的字符数组。
String toLowerCase()
将所有在此字符 String使用默认语言环境的规则,以小写。
String toUpperCase()
将所有在此字符 String使用默认语言环境的规则大写。
String trim()
返回一个字符串,其值为此字符串,并删除任何前导和尾随空格。
public class StringDemo01 {
public static void main(String[] args) {
String s = "abcdefabcd";
//char charAt(int index) 返回 char指定索引处的值。
System.out.println(s.charAt(3)); // d
//String concat(String str) 将指定的字符串连接到该字符串的末尾。
String s1 = s.concat("test");
System.out.println(s1);
//boolean contains(CharSequence s) 当且仅当此字符串包含指定的char值序列时才返回true。
System.out.println(s.contains(s1));
//boolean equals(Object anObject) 将此字符串与指定对象进行比较。
System.out.println("abcd".equals(s));
//boolean equalsIgnoreCase(String anotherString) 将此 String与其他 String比较,忽略大小写
System.out.println("abCdEfAbcd".equalsIgnoreCase(s));
//int indexOf(String str) 返回指定子字符串第一次出现的字符串内的索引。
System.out.println(s.indexOf("d"));
//int indexOf(String str, int fromIndex) 返回指定子串的第一次出现的字符串中的索引,从指定的索引开始。
System.out.println(s.indexOf("d",4));
//boolean isEmpty() 返回 true如果,且仅当 length()为 0 。
System.out.println(s.isEmpty());
//int lastIndexOf(String str) 返回指定子字符串最后一次出现的字符串中的索引。
System.out.println(s.lastIndexOf("d"));
//int lastIndexOf(String str, int fromIndex) 返回指定子字符串的最后一次出现的字符串中的索引,从指定索引开始向后搜索。
System.out.println(s.lastIndexOf("d",2));
//int length() 返回此字符串的长度。
//数组有没有length()方法,字符串有没有length属性
System.out.println(s.length());
//String replace(char oldChar, char newChar)
//将旧字符串替换为新的
//"abcdefabcd"
String s2 = s.replace("abc", "ABC");
System.out.println(s2);
//String replaceAll(String regex, String replacement)
//用给定的替换替换与给定的 regular expression匹配的此字符串的每个子字符串。
//String replaceFirst(String regex, String replacement)
//用给定的替换替换与给定的 regular expression匹配的此字符串的第一个子字符串。
String s3 = s.replaceFirst("abc", "ABC");
System.out.println(s3);
//String[] split(String regex)
//将此字符串分割为给定的 regular expression的匹配。
String[] s4 = s.split("");
System.out.println(Arrays.toString(s4));
String[] s5 = s.split("b");
System.out.println(Arrays.toString(s5));
//String substring(int beginIndex)
//返回一个字符串,该字符串是此字符串的子字符串。
//String substring(int beginIndex, int endIndex)
//返回一个字符串,该字符串是此字符串的子字符串。
System.out.println(s.substring(3));
System.out.println(s.substring(3,6));
//char[] toCharArray()
//将此字符串转换为新的字符数组。
//String toLowerCase()
//将所有在此字符 String使用默认语言环境的规则,以小写。
//String toUpperCase()
//将所有在此字符 String使用默认语言环境的规则大写。
System.out.println(s.toUpperCase());
//String trim()
//返回一个字符串,其值为此字符串,并删除任何前导和尾随空格。
String s6 = " adsag ";
System.out.println(s6);
System.out.println(s6.trim());
}
}
使用String中的方法,
求 : 一串大的字符串"adagjavagawegjavafwnaljavafwjavaafjava"中小字符串"java" 出现的次数。
public class StringDemo02 {
public static void main(String[] args) {
//求 : 一串大的字符串"adagjavagawegjavafwnaljavafwjavaafjava"
// 中小字符串"java" 出现的次数。
//将小字符的长度,带入大字符串中去截取,替换原来的大字符串,如此循环
String str1 = "adagjavagawegjavafwnaljavafwjavaafjava";
String str2 = "java";
//先去找java在大字符串中,第一次出现的位置 ,如果能找到,index != -1
int index = str1.indexOf(str2);
int count = 0;
while (index != -1){
count ++; //统计变量自增
//将大字符串截取后,替换原来的大字符串
str1 = str1.substring(index + str2.length());
index = str1.indexOf(str2);
}
System.out.println(count);
}
}
把字符串 "abcdefg"做翻转后输出
public class StringDemo03 {
public static void main(String[] args) {
//把字符串 "abcdefg"做翻转后输出
//先转数组
//从后向前遍历,做拼接
String s = "abcdefg";
char[] chars = s.toCharArray();
String s1 = "";
for (int i = chars.length-1; i >=0 ; i--) {
s1 += chars[i];
}
System.out.println(s1);
}
}
9.6.6 StringBuffer类
StringBuffer是一个字符串缓冲区类
String类型声明的字符串,不可变,所以每次拼接字符串的时候,都会产生新的引用,所以,使用String来完成字符串拼接,比较消耗资源
所以,Java中提供了这个StringBuffer类,让我们可以用来完成字符串拼接等其他的操作
构造方法
StringBuffer()
构造一个没有字符的字符串缓冲区,初始容量为16个字符。
StringBuffer(CharSequence seq)
构造一个包含与指定的相同字符的字符串缓冲区 CharSequence 。
StringBuffer(int capacity)
构造一个没有字符的字符串缓冲区和指定的初始容量。
StringBuffer(String str)
构造一个初始化为指定字符串内容的字符串缓冲区。
普通方法
StringBuffer append(CharSequence s) 追加指定的 CharSequence到这个序列。
int capacity() 返回当前容量。
StringBuffer delete(int start, int end) 删除此序列的子字符串中的字符。
StringBuffer insert(int offset, String str) 将字符串插入到此字符序列中。
StringBuffer reverse() 导致该字符序列被序列的相反代替。
String toString() 返回表示此顺序中的数据的字符串。
public class StringBufferDemo {
public static void main(String[] args) {
//创建缓冲区对象
StringBuffer sb1 = new StringBuffer("abcd");
System.out.println(sb1);
StringBuffer sb2 = sb1.append("abcdefgh");
System.out.println(sb2);
System.out.println(sb1);
//sb1 == sb2 返回true,说明新的内容是直接添加在原来的内容中的
//也就是堆中的区域并没有改变,地址也没有变化
System.out.println(sb1 == sb2); //true
//空间大小受构建对象时候传入的参数个数影响
System.out.println(sb1.capacity());
//删除也是在原对象基础上删除
StringBuffer sb3 = sb1.delete(4, 6); //删除4,5
System.out.println(sb3);
System.out.println(sb1);
//insert插入,指定位置插入
sb1.insert(2,"hello");
System.out.println(sb1);
//翻转
sb1.reverse();
System.out.println(sb1);
}
}
StringBuffer和String拼接字符串效率对比
public class StringBufferDemo01 {
public static void main(String[] args) {
//给String 拼接 10000次字符串
long start = System.currentTimeMillis();
String s = "hello";
for (int i = 0; i < 10000; i++) {
s += i;
}
long end = System.currentTimeMillis();
System.out.println(end - start);
//给StringBuffer拼接10000次字符串
long start1 = System.currentTimeMillis();
StringBuffer sb1 = new StringBuffer("hello");
for (int i = 0; i < 100000; i++) {
sb1.append(i);
}
long end1 = System.currentTimeMillis();
System.out.println(end1 - start1);
long start2 = System.currentTimeMillis();
StringBuilder sb2 = new StringBuilder("hello");
for (int i = 0; i < 100000; i++) {
sb2.append(i);
}
long end2 = System.currentTimeMillis();
System.out.println(end2 - start2);
}
}
9.6.6.1 String、StringBuffer、StringBuilder的区别
String声明的字符不可变,可以看做是字符串常量,String的内容一旦改变,引用就改变了,可以看成是一个新的引用,所以,String类型做字符串拼接,比较耗时
StringBuffer称为字符串缓冲区,提供了一些方法,可以改变该缓冲区中的数据,但是对应的引用没有改变,相对来说StringBuffer效率更快
StringBuffer和StringBuilder ,他们都是字符串缓冲区,只不过StringBuffer的方法多了一个 synchronized关键字,它是线程安全的。多线程情况下,用StringBuffer,单线程就用StringBuilder
9.6.6.2 String 和 StringBuffer的转换
String类型转换为StringBuffer一般是为了使用StringBuffer类型的方法
将来程序中需要的还是String,所以还得转回来
public class StringBufferDemo02 {
public static void main(String[] args) {
//string ->stringbuffer
String s = "hello";
//直接赋值,强制转换都不行
//StringBuffer sb1 = (StringBuffer) s;
//1,通过构造方法
StringBuffer sb1 = new StringBuffer(s);
//2,通过append方法
StringBuffer sb2 = new StringBuffer();
sb2.append(s);
//StringBuffer -> String
//1,通过String类的构造方法
String s1 = new String(sb1);
//2,通过toStrig()方法
String s2 = sb1.toString();
}
}
让用户输入一段字符串,判断字符串是否是对称字符串,比如 aba ,abcdcba 就是对称字符串
9.6.7 Date类
Date类分为两个包,一个是sql包下的Date,一个是util包下的Date
sql包下的Date一般表示毫秒值,用来连接jdbc的时候,可以直接以字符串形式获取数据库中的时间
util包下的Date,一般用来表示生活中的时间格式,目前学习这个Date
构造方法
Date()
分配一个 Date对象,并初始化它,以当前时间创建一个Date对象
Date(long date)
分配一个 Date对象,传入一个毫秒值,根据毫秒值来创建Date对象
//创建日期类型 :
//1,无参构造
Date date = new Date();
System.out.println(date);
//2,使用毫秒值
long time = System.currentTimeMillis();
Date date1 = new Date(time);
System.out.println(date1);
普通方法
long getTime() 返回当前Date对象的毫秒值
void setTime(long time) 设置此 Date对象以表示1970年1月1日00:00:00 GMT后的 time毫秒的时间点。
String toString() 将此 Date对象转换为 String的形式:
String toLocaleString()
System.out.println(date.getTime()); //毫秒值
System.out.println(date.getYear()+1900);
System.out.println(date.getMonth()+1);
System.out.println(date.getDate());
System.out.println(date.getDay()); //星期几
System.out.println(date.getHours());
System.out.println(date.getMinutes());
System.out.println(date.getSeconds());
9.6.8 DateFormat类
DateFormat是日期/时间格式化子类的抽象类
将来如果需要转换时间日期格式,一般使用它的子类 SimpleDateFormat
SimpleDateFormat是一个具体的类,一般用来完成字符串日期和Date日期类型之间的格式转换
构造方法
SimpleDateFormat()
构造一个 SimpleDateFormat使用默认模式和日期格式符号为默认的 FORMAT区域设置。
SimpleDateFormat(String pattern)
使用给定模式 SimpleDateFormat并使用默认的 FORMAT语言环境的默认日期格式符号。
常用方法
String format(Date date)
将日期格式化成日期/时间字符串。
Date parse(String source)
从给定字符串的开始解析文本以生成日期。
public class DemoFormat {
public static void main(String[] args) throws ParseException {
//通过SimpleDateFormat完成Date和String之间的转换
//Date->String
//创建当前的时间对象
Date date = new Date();
//创建日期格式化对象
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//调用方法
String stringTime = sdf.format(date);
System.out.println(stringTime);
//String-> Date
//声明一个字符串时间格式
String time = "2022-07-22 10:10:10";
Date date1 = sdf.parse(time);
System.out.println(date1);
}
}
计算出生天数
//让用户输入自己的生日,然后看看自己已经出生多少天了
Scanner scanner = new Scanner(System.in);
System.out.println("请输入你的生日,参考格式:1999-11-11");
//接受生日输入
String birthday = scanner.next();
//生日转为Date,然后获取时间戳(毫秒值)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(birthday);
long time = date.getTime(); //获取毫秒值
long timeNow = System.currentTimeMillis();
//计算天数
long day = (timeNow - time) /1000/60/60/24;
System.out.println("天数" + day);
日期转换工具类
//日期工具类
public class DateUtil {
//日期转字符串方法
// 返回值 :String
//参数 :日期,格式
public static String dateToString(Date date,String pattern){
return new SimpleDateFormat(pattern).format(date);
}
//字符串转日期方法
// 返回值 :Date
//参数 :String日期,格式
public static Date stringToDate(String date,String pattern) throws ParseException {
return new SimpleDateFormat(pattern).parse(date);
}
}
9.6.9 Calender
常用的日历抽象类,提供了一些常用日期时间中的属性、以及一些转换的方法
常用方法
int get(int field) 返回给定日历字段的值。
static Calendar getInstance() 使用默认时区和区域设置获取日历对象。
String toString() 返回此日历的字符串表示形式。
public class CalendarDemo {
public static void main(String[] args) throws ParseException {
//Calendar类是抽象类,不能直接new,通过getInstance方法创建对象
Calendar calendar = Calendar.getInstance();
//传入的是Calendar类中的属性
int year = calendar.get(Calendar.YEAR);
//int year1 = calendar.get(1);
System.out.println(year);
//System.out.println(year1);
System.out.println(calendar.get(Calendar.MONTH)+1); //月份+1
System.out.println(calendar.get(Calendar.DATE)); //日期
System.out.println(calendar.get(Calendar.DAY_OF_WEEK)-1); //周几获取-1
System.out.println(calendar.get(Calendar.DAY_OF_YEAR)); //一年的第多少天
System.out.println(calendar.get(Calendar.HOUR)); // 时
System.out.println(calendar.get(Calendar.MINUTE)); //分
System.out.println(calendar.get(Calendar.SECOND)); //秒
//获取某个指定时间
//Date date = DateUtil.stringToDate("2000-10-10", "yyyy-MM-dd");
//calendar.setTime(date);
//System.out.println(calendar.get(Calendar.YEAR));
//System.out.println(calendar.get(Calendar.MONTH)+1); //月份+1
//System.out.println(calendar.get(Calendar.DATE)); //日期
//System.out.println(calendar.get(Calendar.DAY_OF_WEEK)-1); //周几获取-1
//System.out.println(calendar.get(Calendar.DAY_OF_YEAR)); //一年的第多少天
//System.out.println(calendar.get(Calendar.HOUR)); // 时
//System.out.println(calendar.get(Calendar.MINUTE)); //分
//System.out.println(calendar.get(Calendar.SECOND)); //秒
//abstract void add(int field, int amount)
//根据日历的规则,将指定的时间量添加或减去给定的日历字段。
//获取三年前的今天
// calendar.add(Calendar.YEAR,-3);
// System.out.println(calendar.get(Calendar.YEAR));
// System.out.println(calendar.get(Calendar.MONTH)+1);
//3年后的,10天前
calendar.add(Calendar.YEAR,3);
calendar.add(Calendar.DATE,-10);
System.out.println(calendar.get(Calendar.YEAR));
System.out.println(calendar.get(Calendar.MONTH)+1);
System.out.println(calendar.get(Calendar.DATE));
}
}
//利用日历类,完成时间的获取
//在计费系统中,需要计算两个时间的时间差,比如停车时间
// 从 7:30 进入停车场, 8:35分离场,每15分钟收费1块钱
//计算我需要交费多少钱
public class CalendarDemo02 {
public static void main(String[] args) throws ParseException {
//利用日历类,完成时间的获取
//在计费系统中,需要计算两个时间的时间差,比如停车时间
// 从 7:30 进入停车场, 8:35分离场,每15分钟收费1块钱
//计算我需要交费多少钱
// 字符串时间对应的Date对象
String time1 = "07:30";
String time2 = "08:35";
Date startDate = DateUtil.stringToDate(time1, "HH:mm");
Date endDate = DateUtil.stringToDate(time2, "HH:mm");
//设置日历类的时间
Calendar c1 = Calendar.getInstance();
Calendar c2 = Calendar.getInstance();
// 通过日历类分别获取到 开始时间的小时和分钟
c1.setTime(startDate);
int startHour = c1.get(Calendar.HOUR);
int startMinute = c1.get(Calendar.MINUTE);
//获取结束时间的小时和分钟
c2.setTime(endDate);
int endHour = c2.get(Calendar.HOUR);
int endMinute = c2.get(Calendar.MINUTE);
//计算总时间,计算价格
int money = ((endHour * 60 + endMinute) - (startHour * 60 + startMinute))/15 * 1;
System.out.println(money);
}
}
9.6.10 正则表达式
指的是符合一定规则的字符串,用来约束某些信息,在编写的时候,遵守指定的规则
正则的编写规范 :
1,字符格式
a 表示 匹配 a
[abc] 表示匹配 abc ,1次
[^abc] 表示除abc以外的 ,1次
[A-Z] 表示匹配大写字母 1次
[0-9] 表示匹配任意数字1次
[a-z] 表示匹配小写字母1次
[a-zA-Z0-9] 表示匹配大小写字母数字1次
2,次数格式
{n} 匹配指定的n次
{n,m} 匹配n到m次
{n,} 匹配最少n次,最高不限次数
? 一次或者0次
*0次或者多次
+1次或者多次
3,通配符
\转义符 ,将有特殊含义的字符转义为本来的含义
^表示以指定的内容
$表示以指定的内容结尾
\w 表示任意字符 ,相当于 [a-z_A-Z0-9]
\d 表示任意数字 ,相当于[0-9]
public class RegexDemo {
public static void main(String[] args) {
//测试用户输入的手机号码是否符合规范
//声明一个手机号码的正则表达式
String regex = "^[1][3-9]\\d{9}";
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个手机号码:");
String phoneNum = sc.next();
//调用方法,传入正则规则,判断是否符合
boolean b = phoneNum.matches(regex);
if (b){
System.out.println("手机号符合规范");
}else {
System.out.println("手机号不符合规范");
}
}
}
public class RegexDemo02 {
public static void main(String[] args) {
//写一个邮箱的正则表达式 xxx@sohu.com.cn
//String regex = "[0-9a-zA-Z]{3,16}@[0-9a-zA-Z]{2,10}(\\.[a-z]{2,3}){1,2}";
String regex = "\\w{3,16}@\\w{2,10}(\\.\\w{2,3}){1,2}";
Scanner sc = new Scanner(System.in);
System.out.println("输入邮箱:");
String s = sc.next();
boolean b = s.matches(regex);
if (b){
System.out.println("符合规范");
}else {
System.out.println("不符合规范");
}
}
}
10 吃鸡游戏
package com.iweb.airui369.test;
/*
类 玩家 Player,开局的时候,默认血量为100,没有枪
属性 昵称 name
血量 HP
枪 gun
行为 捡枪(枪)
装弹(弹夹,子弹)
上膛(弹夹)
开枪(玩家)
掉血(伤害值)
*/
public class Player {
private String name;
private int HP = 100;
private Gun gun;
public Player() {
}
public Player(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHP() {
return HP;
}
public void setHP(int HP) {
this.HP = HP;
}
// 捡枪(枪)
// 装弹(弹夹,子弹)
// 上膛(弹夹)
// 开枪(玩家)
// 掉血(伤害值)
//捡枪
public void takeGun(Gun gun){
//如果玩家没有枪,给他一把枪
if (this.gun == null){
this.gun = gun;
System.out.println("玩家" + name + "拿到枪!");
}else {
System.out.println("玩家" + name + "已经有枪了!");
}
}
//上膛
public void loadClip(Clips clips){
//有枪才能上弹夹
if (this.gun != null){
//需要通过枪去调用方法
this.gun.loadClips(clips);
System.out.println("玩家" + name + "给枪装上了弹夹!");
}else {
System.out.println("玩家" + name + "还没有拿到枪!");
}
}
//开枪方法
public void shot(Player player){
//有枪才能开枪
if (gun != null){
//弹夹为空
if (gun.getClips() == null){
System.out.println("枪里没有装弹夹!请装弹夹!");
}else {
//弹夹有了,判断弹夹有没有子弹
if (gun.getClips().getNum() == 0){
System.out.println("弹夹中没有子弹!");
}else {
System.out.println("玩家" + name + "开枪打中了" + player.name);
this.gun.shot(player);
}
}
}else {
System.out.println("玩家" + name + "没有枪,不能开枪!");
}
}
public void bloodLoss(int damage) {
//看看当前血量还有多少,到0就死亡
if (this.HP == 0){
System.out.println("玩家" + name + "已死亡!");
}else {
this.HP -= damage; //掉血
System.out.println("玩家" + name + "受到" + damage +
"点伤害,剩余" + this.HP + "血量!");
}
}
}
package com.iweb.airui369.test;
/*
类 枪
属性
弹夹 clips
行为
装弹夹=Player上膛(弹夹)
开枪(玩家) ,如果开枪没打中的话,提示放了一个空枪
*/
public class Gun {
private Clips clips;
public Clips getClips() {
return clips;
}
//开枪
public void shot(Player player){
//如果弹夹不为空,才能开枪
if (clips != null){
//弹夹调用出弹方法
Bullet bullet = clips.popBullet();
//如果子弹不为null,子弹可以击中玩家
if (bullet != null){
bullet.hit(player);
}else {
System.out.println("放了个空枪!");
}
}else {
System.out.println("枪没有弹夹,请上弹夹!");
}
}
public void loadClips(Clips clips) {
this.clips = clips;
System.out.println("枪的弹夹装好了!");
}
}
package com.iweb.airui369.test;
/*
类 弹夹,弹夹默认只能装入30颗子弹
属性 子弹 bullet
行为 装弹(子弹)
出弹()
*/
public class Clips {
private Bullet[] bullets = new Bullet[30];
private int num = 0; //子弹数量
public int getNum() {
return num;
}
//装弹方法
public void putBullet(Bullet bullet){
//如果弹夹有空了,才可以装弹
if (bullets.length == num){
System.out.println("弹夹的子弹是满的,不需要装弹!");
}else {
//从剩下子弹数量的下一位开始装弹
bullets[num] = bullet;
num++;
System.out.println("弹夹已经装了" + num + "颗子弹!");
}
}
//出弹方法
public Bullet popBullet(){
if (num == 0){
System.out.println("弹夹中子弹打完了!");
return null;
}
//每调用一次方法,子弹数量--,其实就是将子弹从数组中拿出来
//子弹是先进后出的结构,所以每次拿的是上面的
Bullet bullet = bullets[num-1];
System.out.println("弹夹中还剩" + --num + "个子弹!");
return bullet;
}
}
package com.iweb.airui369.test;
/*
类 子弹 默认伤害是10,命中玩家后玩家掉血
属性
伤害值 damage
行为
命中 hit(玩家)
*/
public class Bullet {
private int damage = 10; //默认值为10
public void hit(Player player){
player.bloodLoss(damage);
}
}
package com.iweb.airui369.test;
public class Test {
//设计一个吃鸡的游戏
// 类创建好以后,创建两个角色,角色A和角色B, 角色A出场后,拿到枪,
// 装上子弹以后,开枪打角色B,每次开枪需要返回 :
// A开枪打中了B,B的血量还有多少,如果没打中,就提示A放了一个空枪,没有打中B,直到B死亡,流程结束
public static void main(String[] args) {
Player a = new Player("A");
Player b = new Player("B");
//a.shot(b);
//a捡枪
Gun gun = new Gun(); //先创建枪
a.takeGun(gun);
//创建弹夹对象
Clips clips = new Clips();
a.loadClip(clips);
//装子弹
for (int i = 0; i < 30; i++) {
clips.putBullet(new Bullet());
}
//a.shot(b);
for (int i = 0; i < 20; i++) {
if (i % 2 == 0){
a.shot(b);
}else {
System.out.println("A放了空枪,没打中B~");
}
}
}
}
11 异常
11.1 异常
概念: 就是值程序在执行的过程中,可能会出现的问题,称为异常
生活中的异常 :
计划出去玩 : 出发 -> 去公园 ->散步 -> 吃个烧烤 ->回家
出现的问题 : 散步的时候,下雨了,只能终止计划,提前回家。这个时候,下雨,就是一种异常情况
解决方法 : 玩之前查看天气,如果要下雨,带把雨伞 。带着雨伞 就是 可能出现异常的解决方案
程序中的异常: 程序中出现不正常的情况,跳出的提示
比如如下的例子, 正常按照提示,用户可以输入 1-3的数字,但是有些用户如果输入其他的非数字的字符,程序运行就会发生异常
public class Demo01 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入1-3之间的整数:");
int i = scanner.nextInt();
switch (i){
case 1:
System.out.println("星期一");
break;
case 2:
System.out.println("星期二");
break;
case 3:
System.out.println("星期三");
break;
default:
System.out.println("输入错误");
break;
}
}
}
11.1.1 Java中异常相关的类
为了让程序员能够很好的处理异常错误,Java专门提供了异常类 Throwable,用来处理错误及异常
异常体系结构
Thorwable 父类
--Error 子类,错误类 ,表示严重的问题,程序员一般无法解决
--Exception 子类,异常类,是程序员需要去解决的
----RuntimeException 运行时异常,一般都是程序运行时发生的错误,通过修改代码可以防止异常的出现
----非RuntimeException 编译时异常,必须要处理,否则,程序编译不通过
11.1.2 异常的处理
Java中处理异常,通过5个关键字来实现: try 、catch 、 throw 、 throws 、finally
处理方案 :
1,JVM的默认处理:
把异常的名称、原因、位置等信息输出在控制台,然后终止程序运行
2,自己处理
方式1:
11.1.3 try....catch....finally
语法: try{
代码块1;
}catch(ExceptionType1 e){
异常处理代码块1;
}catch(ExceptionType2 e){
异常处理代码块2;
}finally{
最终代码块;
}
如果,将来程序员写的代码,可能会发生异常,就需要将这段代码提前放在try块中。
在catch的后面,跟上将来你的代码中,可能会出现的异常类型类型 ,如果将来真的发生异常了,程序也不会因为异常而终止,只会继续跳转到catch块中继续执行其中的代码。
catch块中的代码,执行完后,会继续执行finally块中的代码
public class Demo02 {
public static void main(String[] args) {
try {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入被除数:");
int num1 = scanner.nextInt();
System.out.println("请输入除数:");
int num2 = scanner.nextInt();
System.out.println(num1 + "/" + num2 + "=" + num1/num2);
}catch (InputMismatchException exception){
System.err.println("输入的数据不符合类型规范!");
}catch (ArithmeticException exception){
System.err.println("除数不能为零");
}finally {
//不管程序有没有异常,这里的代码都会执行
System.out.println("程序结束,感谢使用!");
}
}
}
try..catch..finally注意事项
1,整个语法结构中,try块是必须要写的,catch块和finally块是可选的,但是catch和finally至少得有一个,也可以同时出现
2,可以有多个catch块,如果catch的异常类型,存在父子关系,那么父类异常必须放在子类异常的后面
3,多个catch块必须位于try块后,finally块也必须位于所有的catch后面
try...catch块和 return语句的执行:
1,当try块和catch中,都有return语句时,不会影响finally语句的执行
2,如果finally中,指定了return,并且是有返回值的情况,返回值可能会替换try中的return的返回值,所以,finally中的尽量不要加return
public class Demo03 {
public static void main(String[] args) {
System.out.println(operate());
}
public static int operate(){
int num1 = 0;
int num2 = 1;
try {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入被除数:");
num1 = scanner.nextInt();
System.out.println("请输入除数:");
num2 = scanner.nextInt();
//System.out.println(num1 + "/" + num2 + "=" + num1/num2);
return num1/num2;
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("程序结束!");
//return num1/num2;
}
return 0;
}
}
11.1.4 通过throws,将异常抛出。
把自己处理不了的异常,在方法上声明,告诉调用者,这个方法可能会有异常,需要处理
在方法上,通过throws抛出异常之后,调用者,调用后,需要怎么做?
开发的时候,调用的方法上,存在异常,需要怎么解决 ?
(idea中直接alt+enter,选择以下两个方法中的一个处理即可)
1,将异常继续向上抛;一般平时的代码测试可以用这个方式
2,使用try-catch手动捕获异常,进行处理;将来项目开发,尽量都用这种方式
public class Demo04 {
public static void operate() throws Exception{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入被除数:");
int num1 = scanner.nextInt();
System.out.println("请输入除数:");
int num2 = scanner.nextInt();
System.out.println(num1 + "/" + num2 + "=" + num1/num2);
System.out.println("程序结束!");
}
}
public class Test {
//在方法上继续抛出异常
public static void main(String[] args) throws Exception {
//手动捕获异常,并处理 或者在方法上继续抛出
//try {
Demo04.operate();
//} catch (Exception e) {
// System.err.println("程序发生了异常!");
// e.printStackTrace();
//}
}
}
11.1.5 throw关键字的使用
throw一般是用来方法内部,更多的含义是声明异常
public class Student {
private String name;
private String gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) throws Exception {
if ("男".equals(gender)|| "女".equals(gender)){
this.gender = gender;
}else {
throw new Exception("性别必须是男或者是女!");
}
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
'}';
}
}
public class Test {
//在方法上继续抛出异常
public static void main(String[] args) throws Exception {
Student s = new Student();
s.setName("张三");
s.setGender("妖");
System.out.println(s);
}
}
11.1.6 throws和throw的区别?
1,throws一般声明在方法体上,throw 声明在方法内部
2,throws用来声明一个方法可能抛出的所有的异常信息,表示将来异常的一种可能性,但并不一定发生异常
throw 则是抛出一个具体的异常类型,throw是抛出了某个异常对象
3,throws通常不需要显式的捕获异常,由系统自动将异常抛给上级方法
throw需要用户自己捕获相关异常,然后再对其处理,最后将异常信息抛出
自定义异常:
//自定义异常
public class GenderException extends Exception {
//构造方法
public GenderException(String message) {
super(message);
}
}
public class Test {
//在方法上继续抛出异常
public static void main(String[] args) {
try {
Student s = new Student();
s.setName("张三");
s.setGender("妖");
System.out.println(s);
} catch (GenderException e) {
//e.printStackTrace();
System.err.println("性别输入错误");
}
}
}
12 泛型、集合
12.1 泛型
概念: 有的时候,在使用数据类型的时候,不能明确具体要用的是哪个类型,那么就可以使用泛型
将来泛型可以使用在类、方法、接口 、集合等地方,使用比较广泛
泛型的格式 :
<数据类型>
12.1.1 泛型的使用
泛型使用在方法上
public class Demo01 {
//声明一个泛型的方法
//返回值类型、参数列表
public <T> void show(T t){
System.out.println(t);
}
}
public class Test {
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
demo01.show("hello");
demo01.show(100);
demo01.show(true);
}
}
泛型使用在类上
//将泛型定义在类上
public class Demo02<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
public class Test1 {
public static void main(String[] args) {
//泛型类的测试
//new对象的时候可以不指定泛型
//不指定泛型,默认这个泛型就是Object
Demo02 demo02 = new Demo02();
demo02.setObj("hello");
String s = (String) demo02.getObj();
System.out.println(s);
demo02.setObj(new Integer(30));
Integer i = (Integer) demo02.getObj();
System.out.println(i);
//创建对象的时候,明确指定泛型的类型
Demo02<String> stringDemo02 = new Demo02<>();
stringDemo02.setObj("张三");
String s1 = stringDemo02.getObj();
System.out.println(s1);
//泛型指定了类型后,就不能使用其他类型了
//stringDemo02.setObj(30);
Demo02<Integer> integerDemo02 = new Demo02<>();
integerDemo02.setObj(50); // 直接传入数字,有自动装箱
Integer i1 = integerDemo02.getObj();
System.out.println(i1);
}
}
泛型定义在接口上
//在接口上使用泛型
public interface Demo03<T> {
void show(T t);
}
public class Demo04<T> implements Demo03 {
@Override
public void show(Object o) {
System.out.println(o);
}
}
public class Test2 {
public static void main(String[] args) {
Demo03 d1 = new Demo04();
d1.show("hello");
Demo03<String> d2 = new Demo04<>();
d2.show("hello");
//d2.show(30);
Demo03<Integer> d3 = new Demo04<>();
d3.show(30);
}
}
12.1.1 泛型通配符
<?> ?表示任意类型,没有明确的话,表示Object以及任意的Java类
<? super T> 向上限定,T及其父类
<? extends T> 向下限定,T及其子类
//泛型的通配符的使用
public class Demo05 {
public static void main(String[] args) {
//使用集合的声明
Collection<?> c1 = new ArrayList<Object>();
Collection<?> c2 = new ArrayList<Animal>();
Collection<?> c3 = new ArrayList<Dog>();
//? extends T
//Collection<? extends Animal> c4 = new ArrayList<Object>();
Collection<? extends Animal> c5 = new ArrayList<Animal>();
Collection<? extends Animal> c6 = new ArrayList<Dog>();
//? super T
//Collection<? super Animal> c7 = new ArrayList<Dog>();
Collection<? super Animal> c8 = new ArrayList<Animal>();
Collection<? super Animal> c9 = new ArrayList<Object>();
}
}
12.2 数据结构
12.2.1 栈
12.2.2 队列
12.2.3 数组
12.2.4 链表
12.2.5 树
树是一种数据结构, 由n(n >=1)个节点组成
称为树的原因是它看起来像一个倒置的树木,也就是根在上,叶子在下
特点 :
每个节点都有0个或者多个子节点
没有父节点的节点,称为根节点
每一个非父节点,有且仅有一个父节点
除了根节点以外,每个子节点可以分为多个不相交的子树
学习中讨论较多的,是二叉树,主要学习二叉树
二叉树是树结构的中的一种结构,特点:
每个节点最多两个子树节点
左子树节点和右子树节点,数有顺序的,顺序不能颠倒
二叉树结构,添加、删除、查找数据都很快,是数组和链表两种结构的优化方案,将来处理大批量动态数据非常好用
二叉树结构有很多的扩展数据结构,比如:平衡二叉树、红黑树、B+树....
有些数据可能存放的时候有一定的局限性,这时候,一般会使用其他类型的二叉树
平衡二叉树 :左子树节点和右子树节点相同
红黑树 : 趋近于平衡树,查询速度非常快,查询子节点的最大次数和最小次数不能超过2倍
12.3 集合
概念 :Java中的集合,其实就是一个容器,用来存放Java类的对象。
Java集合分为很多类,有些是方便存入和取出的,有些是方便查找的,不同的功能,抽取出来之后,就形成了集合体系
Java集合体系 :
Collection 接口体系
Map接口体系
12.3.1 Collection体系
Collection的作用 :
主要用来存储Java中的对象,可以存放多个,是一个对象的容器
数组和集合的区别 :
1,长度
数组长度是固定的
集合的长度是可变的
2,内容不同
数组存放的是同一个类型的元素
集合如果没有指定泛型,可以存放不同类型的对象元素(一般都会指定类型)
3,元素的数据类型
数组可以存储基本类型数据,也可以存储引用类型
集合只能存储引用类型数据
Collection的由来 :
集合可以存储多个元素,对于多个元素也是有不同的需求
比如 : 多个元素,不能重复,或者,多个元素按照某个规则排序.....
针对不同的要求,Java提供了很多的集合类,每个集合的数据结构不同的,虽然结构不同,但是它们的功能都可以用来存储对象、判断、取出,也就是它们的方法名都相同
所以,Java中就将集合的共性的内容,不断的往上提取,最终就形成了集合的继承体系
那么把其中常用的集合类,学习掌握就可以了 。
Collection的基础功能 :
1,添加功能
boolean add(E e) 确保此集合包含指定的元素(可选操作)。
boolean addAll(Collection<? extends E> c) 将指定集合中的所有元素添加到此集合(可选操作)。
2,删除功能
void clear() 从此集合中删除所有元素(可选操作)。
boolean remove(Object o) 从该集合中删除指定元素的单个实例(如果存在)(可选操作)。
boolean removeAll(Collection<?> c) 删除指定集合中包含的所有此集合的元素(可选操作)。
3,判断功能
boolean contains(Object o) 如果此集合包含指定的元素,则返回 true 。
boolean containsAll(Collection<?> c) 如果此集合包含指定 集合中的所有元素,则返回true。
boolean isEmpty() 如果此集合不包含元素,则返回 true 。
4,获取功能
Iterator<E> iterator() 返回此集合中的元素的迭代器。
5,长度功能
int size() 返回此集合中的元素数。
public class CollectionDemo {
public static void main(String[] args) {
//使用多态创建一个Collection对象,指向子类对象ArrayList
Collection<String> collection = new ArrayList<>();
//1,添加方法,其实使用的是ArrayList中的add方法,特点遵循ArrayList中的方法特点
collection.add("hello");
collection.add("Java");
collection.add("world");
collection.add("mysql");
collection.add("oracle");
collection.add("hello");
collection.add("Java");
//2,输出集合
System.out.println(collection);
//3,删除元素
//collection.clear();
//System.out.println(collection);
collection.remove("hello"); //删除指定的hello元素
System.out.println(collection);
//4,判断
System.out.println(collection.contains("mysql"));
System.out.println(collection.isEmpty());
//5,长度功能
System.out.println(collection.size());
//6,转换功能,将集合转成数组
//返回值是一个Object[] 数组,是因为不确定集合中具体的类型是什么类型
//Object[] array = collection.toArray();
//System.out.println(Arrays.toString(array));
//7,遍历集合
//增强for循环
for (String s : collection) {
System.out.println(s);
}
System.out.println("------------------------------------");
//通过迭代器iterator对象
//iterator对象中,存在两个方法
//hasNext() 判断迭代器中,是否存在元素
//next() 把迭代器中的元素返回,每次返回下一个元素
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
使用集合完成模拟斗地主发牌案例 :
package com.javaweb.data.day9.fanxing.collections.doudizhu;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class Demo1 {
public static void main(String[] args) {
//创建一个扑克牌集合
Collection<String> poker = new ArrayList<>();
//需要将扑克牌放入集合
//定义两个数组
//花色数组
String[] colors = {"♥","♠","♣","♦"};
String[] numbers = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
//先给扑克集合中,加入大王小王
poker.add("大王");
poker.add("小王");
//将花色和数字组合后,加入扑克集合中
for (int i = 0; i < colors.length; i++) {
for (int j = 0; j < numbers.length; j++) {
poker.add(colors[i]+numbers[j]);
}
}
//创建玩家集合
Collection<String> play1 = new ArrayList<>();
Collection<String> play2 = new ArrayList<>();
Collection<String> play3 = new ArrayList<>();
Collection<String> diPai = new ArrayList<>();
//发牌之前,洗牌
Collections.shuffle((List) poker);
System.out.println(poker);
//开始发牌
for (int i = 0; i < poker.size(); i++) {
//通过下标将每次循环拿到的扑克集合中的牌给取出来
String p = ((List<String>) poker).get(i);
//最后三张牌,放到底牌中
if (i >= 51){
diPai.add(p);
}else if (i % 3 == 0){
play1.add(p);
}else if(i % 3 == 1){
play2.add(p);
}else if (i % 3 == 2){
play3.add(p);
}
}
//底牌的归属(判定有红桃3的玩家拥有底牌)
if (play1.contains("♥3")) {
play1.addAll(diPai);
} else if (play2.contains("♥3")) {
play2.addAll(diPai);
} else if (play3.contains("♥3")) {
play3.addAll(diPai);
}
System.out.println("底牌:" + diPai);
System.out.println("玩家1:" + play1);
System.out.println("玩家2:" + play2);
System.out.println("玩家3:" + play3);
}
}
12.3.2 List接口
List接口是Collection接口的子接口,表示有序的序列。
将来可以通过索引控制元素在List中的位置,可以通过索引访问到List中的元素
List中可以存放 重复的元素 ,并且可以存放null,存放多个null
List接口中提供了 ListIterator,除了实现Iterator接口中的往后遍历的方法,还写了往前遍历的方法
12.3. 3.List集合的常用子类
ArrayList 数组列表
-- 底层结构是数组
LinkedList 链表列表
-- 底层结构是链表
Vector 单列表
-- 底层结构是数组
12.3.4 ArrayList集合
ArrayList特点 :
可变数组实现List接口,相当于一个动态数组,实现所有方法,可以存入任意类型的元素,包括null ,创建ArrayList会有一个容量,来定义将来存放元素的数组的大小
构造方法
ArrayList()
构造一个初始容量为十的空列表。
ArrayList(int initialCapacity)
构造具有指定初始容量的空列表。
常用方法
boolean add(E e)
将指定的元素追加到此列表的末尾。
void add(int index, E element)
在此列表中的指定位置插入指定的元素。
E get(int index)
返回此列表中指定位置的元素。
Iterator<E> iterator()
以正确的顺序返回该列表中的元素的迭代器。
ListIterator<E> listIterator()
返回列表中的列表迭代器(按适当的顺序)。
E remove(int index)
删除该列表中指定位置的元素。
E set(int index, E element)
用指定的元素替换此列表中指定位置的元素。
public class ListDemo {
public static void main(String[] args) {
//创建arrayList对象
ArrayList<String> arrayList = new ArrayList();
//添加
arrayList.add("hello");
arrayList.add("world");
arrayList.add("java");
arrayList.add("hello");
arrayList.add(null); //可以添加null
System.out.println(arrayList);
//arrayList.add(10,"oracle");
arrayList.add(5,"oracle");
arrayList.add(1,"spring");
System.out.println(arrayList);
//get方法,可以获取指定位置的元素
System.out.println(arrayList.get(2));
//System.out.println(arrayList.get(9));
//remove方法,移除指定位置的元素
System.out.println(arrayList.remove(2));
System.out.println(arrayList);
//set方法,替换指定位置元素
arrayList.set(1,"html");
System.out.println(arrayList);
System.out.println("-------------------------");
//遍历
//通过普通for循环遍历
for (int i = 0; i < arrayList.size(); i++) {
System.out.println(arrayList.get(i));
}
System.out.println("-------------------------");
//增强for遍历
for (String s : arrayList) {
System.out.println(s);
}
System.out.println("-------------------------");
//iterator()方法
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println("-------------------------");
//listIterator()方法
ListIterator<String> listIterator = arrayList.listIterator();
//正向遍历
while (listIterator.hasNext()){
System.out.println(listIterator.next());
}
System.out.println("-------------------------");
//反向遍历
while (listIterator.hasPrevious()){
System.out.println(listIterator.previous());
}
}
}
创建一个Student对象,创建3个Student对象,放入集合中,并遍历
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class StudentTest {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student("张三",20));
students.add(new Student("李四",21));
students.add(new Student("王五",22));
System.out.println(students);
//遍历
for (Student student : students) {
System.out.println(student);
}
System.out.println("--------------------------");
Iterator<Student> iterator = students.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
12.4 ArrayList源码解析 面试必问
12.4.1 属性分析:
默认初始容量
private static final int DEFAULT_CAPACITY = 10;
ArrayList空实例共享一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
用于默认大小的空实例的共享空数组实例。
将其与EMPTY_ELEMENTDATA区分开来,用来了解当添加第一个元素的时候需要创建多大的空间
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
真正存储ArrayList中元素的数组
transient Object[] elementData;
ArrayList中的元素个数
private int size;
12.4.2 构造方法
无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
有参构造
12.4.3 普通方法
添加方法
首先,我们来看看这个add方法:
public boolean add(E e) {
//确认list容量,看看是否需要容量增加
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素放到已有元素的下一个位置
elementData[size++] = e;
return true;
}
接下来我们来看看这个小容量(+1)是否满足我们的需求:
// 获取到满足需求的最小容量
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
先将这个容量值传入calculateCapacity方法,计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// elementData是空列表
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 扩容数组到DEFAULT_CAPACITY(10)
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// return size+1
return minCapacity;
}
然后判断是否需要扩容,如果需要扩容,就调用grow()方法,进行扩容
// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
//调用grow方法进行真正地扩容
grow(minCapacity);
}
步骤解析:
1,当添加第1个元素到 ArrayList 中时,minCapacity 为size+1=1,elementData还是空的list,elementData.length=0 ,所有会执行return Math.max(DEFAULT_CAPACITY, minCapacity) 返回值为DEFAULT_CAPACITY,也就是10 ;
minCapacity变为10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法真正扩容。
2,当添加第2个元素时,minCapacity 为 size+1 =2,由于elementData.length在添加第一个元素后已经扩容成10了。此时,minCapacity - elementData.length > 0 不成立,不会执行grow(minCapacity) 方法,即不会扩容。
3,添加第 3、4、5......到第10个元素时,依然不会扩容,数组容量还是为10。
直到添加第11个元素,minCapacity - elementData.length > 0 成立,执行grow 方法进行扩容。
所以,接下来看看grow()是怎么实现的~
private void grow(int minCapacity) {//11
// oldCapacity为旧容量
int oldCapacity = elementData.length;//10
// 位移运算符比普通运算符的运算快很多。>>表示将oldCapacity右移一位,(oldCapacity >> 1)相当于oldCapacity /2
// 将新容量更新为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若新容量还是小于最小需求容量
if (newCapacity - minCapacity < 0)
// 直接最小需求容量当作数组的新容量
newCapacity = minCapacity;
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
// 新容量大于MAX_ARRAY_SIZE,
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 执行hugeCapacity()方法
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList创建空实例以及扩容总结:
1,创建无参构造的ArrayList,默认创建的是空数组,长度为0
2,当往这个ArrayList对象中,放入第一个元素之后,数组扩容,扩容到默认长度10
3,当放入到第11个元素的时候,数组继续扩容,扩容到原数组长度的1.5倍,即将10扩容到15
12.5 iterator()方法
iterator()方法,来自于Collection接口 的 父接口 Iterable 接口
Iterable接口其实就定义了Iterator<T> iterator();这个抽象方法,其实就是用来指定将来接口中迭代元素的规则
ArrayList 实现了List接口,List接口,继承自Collection接口,Collection接口中,存在iterator()方法
ArrayList 类中,必定重写了 iterator()方法,因为iterator()是父接口的抽象方法
ArrayList中的方法如下 :
返回值是 new Itr(), 说明Itr是 Iterator接口的自实现类,那么既然Itr是实现类,必定会重写Iterator接口中的抽象方法 hasNext() ,next()
通过查看源码,发现 Itr类,是ArrayList中的一个内部类
分析这个内部类的源码:
有一个属性 cursor ,用来记录每次返回的下一个元素的索引
重写的hasNext()方法,用来判断 cursor是否返回到最后了,和list的size相同,就结束
重写的next()方法,用来将指定的元素返回,每次返回后,将cursor指向下一个元素
public E next() {
checkForComodification();
int i = cursor;
Object[] elementData = ArrayList.this.elementData;
cursor = i + 1;
return (E) elementData[lastRet = i];
}
12.6 ArrayList总结:
1,ArrayList底层是动态扩容的数组,使用无参构造创建ArrayList的时候,其实创建的是一个空数组,当放入第一个元素开始,扩容为默认容量10
2,当原来的容量放满之后,放入下一个元素的时候,数组开始扩容,扩容到原来容量的1.5倍,每次扩容都会进行底层的数组复制的操作
3,频繁的扩容,会降低ArrayList的效率 ,所以,如果知道要存入多少个元素,那么在声明的时候就直接指定容量,可以加大ArrayList的效率
4,ArrayList 是有序的,指的是存取的顺序有序,可以存放 null值,可以存放重复的元素
5,ArrayList是线程不安全的
12.7 Vector和ArrayList的区别
Vector是JDK1.2的类,是一个比较老旧的集合
Vector的底层也是数组,和ArrayList的区别是 :
Vector是线程同步的(安全的),它类中的方法上都加了synchronized来修饰,所以它的效率低一点
Vector的扩容,每次扩容2倍
12.8 LinkedList的使用
LinkedList底层是双向链表
既然是双向链表,优点是往集合中插入和删除元素较快,查找指定元素效率低一些
LinkedList还实现了Deque接口,所以LinkedList中,也有操作队列、栈的方法
LinkedList既然是链表的底层,那么必然存在Node 节点 ,在这里声明了一个内部类Node用来表示节点:
LinkedList常用的方法
public class LinkedListDemo {
public static void main(String[] args) {
//创建LinkedList对象,泛型是String
/* LinkedList<String> linkedList = new LinkedList<>();
//调用普通的list中的方法
linkedList.add("hello");
linkedList.add("world");
linkedList.add("java");
linkedList.add("mysql");
linkedList.add(4,"spring");
System.out.println(linkedList);
String s = linkedList.get(3);
System.out.println(s);
linkedList.remove(1);
System.out.println(linkedList);
//遍历
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
System.out.println("--------------");
for (String s1 : linkedList) {
System.out.println(s1);
}
System.out.println("--------------");
Iterator<String> iterator = linkedList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
*/
//栈结构相关的方法
//E pop()
//从此列表表示的堆栈中弹出一个元素。
//void push(E e)
//将元素推送到由此列表表示的堆栈上。
/* LinkedList<String> list1 = new LinkedList<>();
list1.push("hello");
list1.push("world");
list1.push("java");
list1.push("mysql");
System.out.println(list1);
String pop = list1.pop();
System.out.println(pop); //mysql
System.out.println(list1);
*/
//队列结构相关方法
//void addFirst(E e)
//在该列表开头插入指定的元素。
//void addLast(E e)
//将指定的元素追加到此列表的末尾。
//E getFirst()
//返回此列表中的第一个元素。
//E getLast()
//返回此列表中的最后一个元素。
//E pollFirst()
//检索并删除此列表的第一个元素,如果此列表为空,则返回 null 。
//E pollLast()
//检索并删除此列表的最后一个元素,如果此列表为空,则返回 null 。
//E removeFirst()
//从此列表中删除并返回第一个元素。
//E removeLast()
//从此列表中删除并返回最后一个元素。
LinkedList<String> linkedList1 = new LinkedList<>();
linkedList1.addFirst("1");
linkedList1.addFirst("2");
linkedList1.addFirst("3");
linkedList1.addLast("4");
linkedList1.addLast("5");
linkedList1.addLast("6");
System.out.println(linkedList1);
System.out.println(linkedList1.getFirst()); //3
System.out.println(linkedList1.getLast());//6
System.out.println(linkedList1);
String s1 = linkedList1.pollFirst();
System.out.println(s1);
String s2 = linkedList1.pollLast();
System.out.println(s2);
System.out.println(linkedList1);
linkedList1.removeFirst();
linkedList1.removeLast();
System.out.println(linkedList1);
}
}
12.9 LinkedList源码解析:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//记录当前双向链表的长度
transient int size = 0;
//记录当前双向链表的头节点
transient Node<E> first;
//记录当前双向链表的尾节点
transient Node<E> last;
12.9.1 构造方法
LinkedList的构造方法有两个:
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
12.9.2 普通方法
add方法
add方法实际上就是往链表最后添加元素
public boolean add(E e) {
linkLast(e);
return true;
}
addLast方法,同add方法
public void addLast(E e) {
linkLast(e);
}
linkLast(E)方法可以在当前双向链表的尾节点之后添加一个新的节点,并且调整last属性的指向位置
void linkLast(E e) {
//使用一个临时变量来记录操作前的last属性信息
final Node<E> l = last;
//创建一个新的节点,item属性值为e,新节点的前置对象指向原来的尾节点,后置节点为null
final Node<E> newNode = new Node<>(l, e, null);
//因为要在双向链表的尾节点添加新的节点,将last属性中的信息重新设置
last = newNode;
//条件成立,说明双向链表没有任何节点
if (l == null)
//将first节点指向新的节点,first和last都同时指向同一个节点
first = newNode;
else
//不成立,双向链表至少有一个节点,将原来的尾节点的后置节点指向新的尾节点
l.next = newNode;
//双向链表长度 + 1
size++;
//linkedList集合的操作次数 + 1
modCount++;
}
addFirst方法
public void addFirst(E e) {
linkFirst(e);
}
/**
* Links e as first element.
*/
private void linkFirst(E e) {
//使用一个临时的变量记录操作前first属性的信息
final Node<E> f = first;
//创建一个数据信息为e的新节点,该节点前置节点引用为null,后置节点引用指向原先的头节点
final Node<E> newNode = new Node<>(null, e, f);
//因为要在双向链表头部添加新的节点,将first属性中的信息重新设置
first = newNode;
//条件成立,说明双向链表没有任何节点
if (f == null)
//将last节点也指向新的节点,这样first和last节点属性同时指向同一个节点
last = newNode;
else
//不成立,说明双向链表至少有一个节点,只需要把原来的头节点的前置节点引用指向新的头节点
f.prev = newNode;
//双向链表长度 + 1
size++;
//linkedList集合的操作次数 + 1
modCount++;
}
remove方法
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
实际调用的是unlink()方法
E unlink(Node<E> x) {
// assert x != null;
//定义一个element变量,记录当前节点中的数据对象,以便方法最后返回
final E element = x.item;
//创建一个next节点,记录当前节点中的后置节点引用,可能为null
final Node<E> next = x.next;
//创建一个prev节点,记录当前节点中的前置节点引用,可能为null
final Node<E> prev = x.prev;
//如果条件成立,说明被移除的x节点是双向链表的头节点
if (prev == null) {
//将x的后置节点设置为新的头节点
first = next;
} else {
//将x的前置节点中的后置节点设置为移除的x节点的后置节点
prev.next = next;
//将移除的x节点的前置节点设置为null
x.prev = null;
}
//如果条件成立,说明被移除的x节点是双向链表的尾节点
if (next == null) {
//将移除的x的节点的前置节点设置为新的尾节点
last = prev;
} else {
//将x的后置节点中的前置节点设置为移除x节点的前置节点
next.prev = prev;
//将移除的x节点的后置节点设置为null
x.next = null;
}
//将移除的x节点中的数据对象设置为null
x.item = null;
//双向链表长度 - 1
size--;
//LinkedList集合操作次数 + 1
modCount++;
return element;
}
get方法
可以看到get方法实现就两段代码:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
我们进去看一下具体的实现是怎么样的:
Node<E> node(int index) {
// assert isElementIndex(index);
//下标长度小于一半,就从头遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//否则就从尾部遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
set方法
set方法和get方法其实差不多,根据下标来判断是从头遍历还是从尾遍历
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
12.9.3 List总结
常用的子类ArrayList、LinkedList、Vector,有序(存取有序)、可以存放null,可以重复
ArrayList:
底层是动态扩容数组、初始容量默认是0,放入元素变为10,存放第11个元素开始扩容,扩容到原来的1.5倍,查询效率快,增删效率较慢,因为增删的时候,需要数组的拷贝(native方法,c/c++实现)
LinkedList:
底层是双向链表,查询效率较低,从头到尾遍历,增删效率较高
Vector :
底层也是数组,初始容量默认为10,每次扩容2倍,方法都是同步的,效率相对较慢
一般将来,查询多用ArrayList,增删多用LinkedList
其实增加元素的时候,如果一直是增加到末尾的话,使用ArrayList也很快,一直删除末尾元素,ArrayList也快
12.10 Map
之前的Collection,称为集合,使用它可以存放单个的对象,可以快速找到我们想要的元素
而Map称为映射,它可以存放一对键值数据,一对key : value数据
比如 : name:张三,age:20 ,1001: Student{张三,20,1班}
12.10.1 Map和Collection的区别:
1,Map中的元素,是成对出现的,Map中的键是唯一的,值是可以重复的,可以有null值
2,Collection中的元素,都是单个出现的,Collection的子类,set中元素是唯一的,不能重复,List是可以重复的
12.10.2 Map的功能
1.添加功能
V put(K key, V value)
将指定的值与该映射中的指定键相关联(可选操作)。
2,获取功能
V get(Object key)
返回到指定键所映射的值,或 null如果此映射包含该键的映射。
Set<Map.Entry<K,V>> entrySet()
返回此Map中包含的映射的Set视图。
Set<K> keySet()
返回此Map中包含的键的Set视图。
Collection<V> values()
返回此地图中包含的值的Collection视图。
3,判断功能
boolean isEmpty()
如果此Map不包含键值映射,则返回 true 。
boolean containsKey(Object key)
如果此映射包含指定键的映射,则返回 true 。
boolean containsValue(Object value)
如果此地图将一个或多个键映射到指定的值,则返回 true 。
4,删除功能
V remove(Object key)
如果存在(从可选的操作),从该地图中删除一个键的映射。
void clear()
从该地图中删除所有的映射(可选操作)。
5,长度功能
int size()
返回此地图中键值映射的数量。
12.10.3 Map的常用子类 HashMap
HashMap基于哈希表的实现的Map接口。
此实现提供了所有可选的Map操作,并允许null的值和null键,null键不能重复。线程不安全的,存放数据是无序的
构造方法
HashMap()
构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。
HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子
public class MapDemo {
public static void main(String[] args) {
Map<String, String> map = new HashMap() ; //键值都是String
//通过put方法,完成map数据的存放
map.put("one","hello");
map.put("one","mysql");
map.put("two","java");
map.put("three","world");
map.put("1","oracle");
map.put(null,"spring"); //可以把null作为键
map.put(null,"Linux"); //可以把null作为键,相同的键对应的值被覆盖
System.out.println(map);
//获取
System.out.println(map.get("one"));
System.out.println(map.get("2")); //获取不存在,返回null
//判断
System.out.println(map.containsKey("one")); //true
System.out.println(map.containsValue("hello"));//false
//移除
map.remove("one");
System.out.println(map);
//长度
System.out.println(map.size());
System.out.println("------------------------");
//遍历
//第一种:先获取key,通过key再获取值
Set<String> set = map.keySet(); //先获取所有key的set集合
for (String key : set) {
//遍历set
System.out.println(key + "----" + map.get(key));
}
System.out.println("------------------------");
Map<Integer, String> map1 = new HashMap() ;//键是Integer,值是String
map1.put(1,"hello");
map1.put(2,"java");
map1.put(3,"oracle");
System.out.println(map1);
//分开分别获取key和value
for (Integer key : map1.keySet()) {
System.out.println("key:" + key);
}
for (String value : map1.values()) {
System.out.println("value:" + value);
}
System.out.println("------------------------");
Map<String, Object> map2 = new HashMap() ;//键是String,值是对象
map2.put("name","张三");
map2.put("age",20);
map2.put("1001",new Student("李四",22,"1班"));
System.out.println(map2);
//使用entrySet()方法来实现遍历
Set<Map.Entry<String, Object>> entries = map2.entrySet();
//使用增强for遍历返回的entrySet
for (Map.Entry<String, Object> entry : entries) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
System.out.println("------------------------");
//使用iterator遍历返回的entrySet
Iterator<Map.Entry<String, Object>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<String, Object> entry = iterator.next();
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
}
12.10.4 HashMap的源码解析:
HashMap的底层结构 :jdk1.7 数组+链表 ,jdk1.8 之后 数组+链表+红黑树
数组和链表都可以按照用户的意愿来排列元素的次序,是有序(存取有序),
链表中也有缺点,想要获取某个元素,要访问所有元素,直到找到位置,这是比较耗时的
数组+链表的组合,可以看做是一个新的数据结构:散列表 (hash表)
这个结构在存储的时候,不在意元素的顺序,也能够快速的找到元素。
12.10.4.1 散列表的原理 :
将每个对象 计算出一个 数值,这个数值称为散列码(hash码),根据这些散列吗,计算数据保存的位置。
每个散列表,在Java中,也称之为 桶 。
一个桶上,多个对象存储的时候可能会遇到散列码相同的情况,这个是,散列码相同的对象,就得存储到同一个位置上。这种情况称为 :散列冲突(hash冲突)
发生散列冲突后,就需要将存放的数据,进行比较,看看发生冲突的对象是否是同一个对象,如果是同一个对象,将原来的替换,如果不是,将对象添加在桶的链表上
12.10.4.2 HashMap的基本说明
注释上的说明:
基于哈希表的实现的Map接口,允许null的值和null键,不能保证Map的顺序
HashMap的对象有两个影响其性能的参数,初始容量16和负载因子0.75
初始容量太高和负载因子太低,都是不太好的,会影响性能
当存储的数量达到了16*0.75=12的时候,哈希表开始扩容,扩容2倍
如果要存储很多数据在Map中,可以在初始化的时候,把容量设置的高一些,可以提升效率
HashMap是线程不安全的
属性的说明:
默认的初始容量值是16,必须是2的n次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最高容量是2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
默认的负载因子是0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
内部链表结构转成红黑树结构的节点,链表的数量=8个
static final int TREEIFY_THRESHOLD = 8;
当树的结构中数量=6了,退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
链表结构转为红黑树另一个要满足的条件,桶的数量=64
static final int MIN_TREEIFY_CAPACITY = 64;
构造方法
无参构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
有参构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
//如果初始值、加载因子小于0或者是非数字,就抛异常
//如果初始值超过了最大范围,就把初始值设为最大值
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor(initialCapacity) 用来将传入的数值转成2的n次方后,返回
HashMap中内容解析 · 语雀 《HashMap中内容解析》
12.10.4.3 普通方法
put方法:
1,将key和value传入put方法后,先把key传入hash方法后,完成计算,在传入putVal这个方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
2,key传入hash方法后,将key的hashCode值,和key的hashCode值右移16位后,做的位异或运算
这种计算方式是为了减少hash冲突。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3,将key经过hash计算后的值、key本身的值、value值,传入putVal
这个是putVal方法中的一个代码片段,表示存放元素的时候,将数组(hashmap对象)的长度-1之后,和key计算出来的hash值做运算,通过运算结果判断这个数组的下标位置是否存在元素,如果没有元素,就将元素传入新节点,创建后,放到这个数组的下标位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
代码拆分:
i = (n - 1) & hash; //计算出来数组的下标
p = tab[i]; //通过下标取出来的值
if(p == null){ //如果这个数组的下标元素为null,那么将这个下标位置指定元素
tab[i] = newNode(hash, key, value, null);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步骤①:tab为空则创建
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index,并对null做处理
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 步骤③:节点key存在,直接覆盖value
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// 步骤④:判断该链为红黑树
// hash值不相等,即key不相等;为红黑树结点
// 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node,
//e可能为null
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:该链为链表
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
//判断该链表尾部指针是不是空的
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
//判断链表的长度是否达到转化红黑树的临界值,临界值为8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表结构转树形结构
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,
// 返回新来的value这个值
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 步骤⑥:超过最大容量就扩容
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
12.10.4.5 HashMap中怎么使用散列表存放元素
1,先判断键值对数组table[i]是否为null或者为空,如果是空,执行reisze()方法进行扩容
2, 根据键值key计算hash值,得到插入的数组索引位置 i,如果table[i] == null,直接创建节点,插入到数组下标i位置 ,然后计算是否需要扩容,如果table[i] 不为空,就继续下一步
3,判断table[i] 的首个元素是否和key一样,如果相同(通过hashCode值和equals判断),直接把value覆盖,如果不同,继续下一步
4,判断table[i] 是否是一颗红黑树,如果是红黑树,直接在树上插入键值对数据,否则继续下一步
5,遍历table[i],判断元素的next节点是否为null,如果是null,就把新的节点放入链表中,并让上一个节点的next,指向新节点,接着判断链表的长度是否大于8,如果大于8,把链表转为红黑树
6,插入成功,判断实际的size值是否超过了 扩容的阈值,如果超过,则进行扩容
12.10.4.6 HashMap中,如何扩容?
通过resize()方法完成扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
threshold = Integer.MAX_VALUE;
return oldTab;//返回
}//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
}
// 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
// 直接将该值赋给新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的threshold = 新的cap * 0.75
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 计算出新的数组长度后赋给当前成员变量table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
table = newTab;//将新数组的值复制给旧的hash桶数组
// 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
if (oldTab != null) {
// 遍历新数组的所有桶下标
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
oldTab[j] = null;
// 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
if (e.next == null)
// 用同样的hash映射算法把该元素加入新的数组
newTab[e.hash & (newCap - 1)] = e;
// 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// e是链表的头并且e.next!=null,那么处理链表中元素重排
else { // preserve order
// loHead,loTail 代表扩容后不用变换下标,见注1
Node<K,V> loHead = null, loTail = null;
// hiHead,hiTail 代表扩容后变换下标,见注1
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
// 代表下标保持不变的链表的头元素
loHead = e;
else
// loTail.next指向当前e
loTail.next = e;
// loTail指向当前的元素e
// 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
// 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
// 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
loTail = e;
}
else {
if (hiTail == null)
// 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
12.10.4.7 get方法步骤
先计算key的hash值,调用getNode()方法获取到对应的value
getNode() :先计算hash是否在hash表上,如果在桶的首位能找到,直接返回,否则看看是否是红黑树,如果是,就在树中找,不是就遍历链表
12.10.4.8 remove()方法
通过计算key的hash值删除value
具体通过removeNode()方法实现:
判断桶是否为空,映射的哈希值是否存在,如果在桶的首位找到对应的元素,记录下来,
不在首位就去红黑树或者链表中 去找到,找到对应的节点了,将内容删除
12.10.4.9 Map.Entry对象
Map中没有迭代器对象,所以Map的遍历只能通过将键值返回到集合中的方式,完成遍历
第一种遍历方式,通过Map子代的keySet()方法和values()方法
keySet()方法用来返回所有的key的set集合,可以用来获取到所有的key
values()方法,用来返回所有的value的集合,然后可以用来获取所有的value
第二种遍历方式,通过Map中的entrySet()方法
Set<Map.Entry<K,V>> entrySet()
这个方法方法,map对象调用后,也是返回一个set集合,但是这个set集合中,存放的是Map.Entry对象
Map.Entry是Map接口的内部接口,里面提供了getKey()和getValue()两个抽象方法,既然存在这两个方法,那么返回的set集合中的每个Entry对象,都可以通过这两个方法,获取键和值
通过查看源码发现,HashMap中的 内部类 Node,实现了Map.Entry接口
static class Node<K,V> implements Map.Entry<K,V>
这个Node实际上又是存放HashMap中键值数据的节点,所以说,其实每个Map.Entry对象就是一对键值数据的存放对象(通过它的自实现类Node来实现数据存放)
那既然实现了Map.Entry接口,Node类必然实现了接口中的方法,getKey()和getValue()方法
因为key和value 是Node类的属性,所以,直接以getter方法的形式返回属性值就可以了。
public final K getKey() { return key; }
public final V getValue() { return value; }
12.10.5 HashMap和HashTable的区别
从存储结构来讲,HashTable除了没有红黑树,其他结构一样,HashTable扩容 是 2n+1,它和HashMap最大的不同是,HashTable是线程安全的,不允许key和value为null,HashTable是一个过时的集合了,所以将来如果需要使用线程安全的HashMap,一般使用ConcurrentHashMap
12.10.6 HashMap总结
1,JDK8中,HashMap底层结构是 :数组+单向链表 + 红黑树, 无序、可以存放null值,null键,键不能重复
2,HashMap的初始容量是16,负载因子默认是0.75,当容量达到16*0.75=12的时候,会扩容
3,初始容量和负载因子都可以自己设置 ,负载因子设置的过大和过小都不太好,
过大的话,会减少扩容,增加哈希冲突,
过小可以减少冲突,扩容次数变多
4,如果需要存入大量元素,那么可以指定一个特定的初始容量,HashMap的容量值都是2的n次方
5,当散列表中链表的数量超过8,并且散列表的容量超过了64,链表会进行转树的操作
12.10.7 LinkedHashMap
继承结构
注释说明 :
1,底层是 散列表(数组+链表) + 双向链表
2,允许为null,线程不安全
3,插入顺序有序的
4,负载因子和初始容量影响LinkedHashMap的性能
构造方法
LinkedHashMap()
构造具有默认初始容量(16)和负载因子(0.75)的空插入创建 LinkedHashMap实例。
LinkedHashMap(int initialCapacity)
构造具有指定初始容量和默认负载因子(0.75)的空插入创建 LinkedHashMap实例。
LinkedHashMap(int initialCapacity, float loadFactor)
构造具有指定初始容量和负载因子的空插入创建 LinkedHashMap实例。
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
accessOrder - 创建模式 - true的访问顺序, false的插入顺序
构造一个空的 LinkedHashMap实例,具有指定的初始容量,负载因子和创建模式。
public class HashMapDemo {
public static void main(String[] args) {
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
//添加元素
linkedHashMap.put("one","hello");
linkedHashMap.put("two","world");
linkedHashMap.put("three","java");
linkedHashMap.put("four","oracle");
//获取
System.out.println(linkedHashMap.get("two"));
//判断
System.out.println(linkedHashMap.containsKey("one"));
System.out.println(linkedHashMap.containsValue("hello"));
//移除
linkedHashMap.remove("two");
System.out.println(linkedHashMap);
//遍历
Set<String> set = linkedHashMap.keySet();
for (String key : set) {
System.out.println(key + " -- " + linkedHashMap.get(key));
}
//通过entrySet完成遍历
}
}
accessOrder - 创建模式 - true的访问顺序, false的插入顺序
public class LinkedHashMapDemo {
public static void main(String[] args) {
//accessOrder - 创建模式 - true的访问顺序, false的插入顺序
LinkedHashMap<Integer, String> map =
new LinkedHashMap(16,0.75f,true);
int i = 1;
map.put(i++,"hello1");
map.put(i++,"hello2");
map.put(i++,"hello3");
map.put(i++,"hello4");
map.put(i++,"hello5");
//访问之后,元素的顺序被改变
System.out.println(map.get(3));
System.out.println(map.get(1));
//Set<Integer> sets = map.keySet();
//for (Integer key : sets) {
// System.out.println(key + "--" + map.get(key)) ;
//}
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
}
12.10.7.1 LinkedHashMap总结
LinkedHashMap是HashMap的子类,LinkedHashMap比HashMap多了一个双向链表的结构
LinkedHashMap大部分方法都是来自HashMap
它们之间很多的地方都体现了多态的使用
LinkedHashMap可以设置两种遍历顺序:
访问顺序 和 插入顺序,默认是插入顺序的
12.10.8 TreeMap
注释说明 :
TreeMap实现了NavigableMap接口,NavigableMap继承了SortedMap,使用TreeMap有序
TreeMap的层是红黑树
线程不同步的
使用Comparator或者Comparable实现键的比较,完成最终数据的排序
构造方法
TreeMap()
使用其键的自然排序构造一个新的空树状图。
TreeMap(Comparator<? super K> comparator)
构造一个新的,空的树图,按照给定的比较器排序。
TreeMap(Map<? extends K,? extends V> m)
构造一个新的树状图,其中包含与给定地图相同的映射,根据其键的 自然顺序进行排序 。
TreeMap(SortedMap<K,? extends V> m)
构造一个包含相同映射并使用与指定排序映射相同顺序的新树映射。
public class TreeMapDemo {
public static void main(String[] args) {
//用无参构造创建的TreeMap,默认使用自然排序
TreeMap<String, String> map = new TreeMap<>();
//添加元素
map.put("1","hello");
map.put("2","java");
map.put("4","world");
map.put("5","spring");
map.put("3","list");
System.out.println(map);
//构造TreeMap对象的时候,键必须要可以互相比较
//如果传入键的时候键的数据类型不一致,可能会导致异常的发生
TreeMap<Object, String> map1 = new TreeMap<>();
//map1.put(1,"hello");
//map1.put("a","java");
//map1.put(new Student(),"oracle");
//System.out.println(map1);
//使用对象作为键,作为键的对象,必须要实现Comparable(比较器)接口
//重写其中的CompareTo()方法
TreeMap<Student, String> map2 = new TreeMap<>();
map2.put(new Student("jack",20,"1班"),"hello");
map2.put(new Student("lucy",22,"1班"),"oracle");
map2.put(new Student("zs",22,"2班"),"java");
map2.put(new Student("lisi",24,"2班"),"mysql");
map2.put(new Student("wangwu",21,"2班"),"spring");
System.out.println(map2);
}
}
public class Student implements Comparable<Student> {
private String name;
private int age;
private String className;
public Student() {
}
public Student(String name, int age, String className) {
this.name = name;
this.age = age;
this.className = className;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
if (age != student.age) return false;
if (name != null ? !name.equals(student.name) : student.name != null) return false;
return className != null ? className.equals(student.className) : student.className == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
result = 31 * result + (className != null ? className.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", className='" + className + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
if (o.age> this.age){
return -1;
}
if (o.age < this.age){
return 1;
}
if (o.name.compareTo(this.name) > 0){
return -1;
}
if (o.name.compareTo(this.name) < 0){
return 1;
}
if (o.className.compareTo(this.className) > 0){
return -1;
}
if (o.className.compareTo(this.className) < 0){
return 1;
}
return 0;
}
}
构造TreeMap对象的时候,传入Comparator对象
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TreeMapDemo01 {
public static void main(String[] args) {
TreeMap<User, String> treeMap =
new TreeMap<>(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
if (o1.getAge() > o2.getAge()){
return 1;
}
if (o1.getAge() < o2.getAge()){
return -1;
}
if (o1.getName().compareTo(o2.getName()) > 0){
return 1;
}
if (o1.getName().compareTo(o2.getName()) < 0){
return -1;
}
return 0;
}
});
treeMap.put(new User("jack",20),"java");
treeMap.put(new User("tom",22),"hello");
treeMap.put(new User("tony",19),"hello");
treeMap.put(new User("lucy",22),"hello");
System.out.println(treeMap);
//遍历
Set<Map.Entry<User, String>> entries = treeMap.entrySet();
for (Map.Entry<User, String> entry : entries) {
System.out.println(entry.getKey() + "--" + entry.getValue());
}
}
}
TreeMap的总结
1,底层是红黑树,能够实现该Map集合有序(自然排序)
2,key不能为null
3,可以在构造方法中传入Comparator对象,实现比较方法,也可以让键对象实现Comparable接口,并重写compareTo()方法来实现 键的自然排序
4,TreeMap也是线程不安全的
12.11 Set集合
Set集合中的常用方法基本上和Collection中差不多
Set集合的特点 : 元素不可以重复,无序(有些子类是有序的),只能存放一个null(也有不能存放null的子类)
Set集合常用子类 :
HashSet : 底层结构是哈希表(数组+链表)
TreeSet:底层是红黑树,有序(自然排序)
LinkedHashSet:底层是哈希表 + 链表
HashSet解析
注释说明 :
1,HashSet实现了Set接口,不保证元素的顺序,允许1一个null元素,非同步的,初始容量影响性能
2,底层实际上是一个HashMap对象
构造方法
HashSet()
构造一个新的空集合; 背景HashMap实例具有默认初始容量(16)和负载因子(0.75)。
HashSet(Collection<? extends E> c)
构造一个包含指定集合中的元素的新集合。
HashSet(int initialCapacity)
构造一个新的空集合; 背景HashMap实例具有指定的初始容量和默认负载因子(0.75)。
HashSet(int initialCapacity, float loadFactor)
构造一个新的空集合; 背景HashMap实例具有指定的初始容量和指定的负载因子。
从源码能看到,HashSet在创建的时候,都是通过HashMap来实现的
HashSet中是如何处理创建的HashMap对象的键值的?
通过查看HashSet中的add方法发现,其实add()方法,就是HashMap的put方法
set中通过add方法加入的元素,都成为了对应的map对象的键
map对象中的值都是一个常量PRESENT,这个常量其实是一个空对象(new Object)
所以,其实HashSet中的数据,就是一个HashMap的键,添加、删除、获取等方法,都是通过HashMap来实现的
public class SetDemo01 {
public static void main(String[] args) {
//创建一个hashSet
HashSet<String> set = new HashSet<>();
//存放元素,无序存放,不能重复
set.add("hello");
set.add("hello");
set.add("JAVA");
set.add("WORLD");
set.add(null);
set.add(null);
System.out.println(set);
//遍历
for (String s : set) {
System.out.println(s);
}
System.out.println("----------------");
//iterator遍历
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
HashSet的存放原理
先获取元素的hashCode()值,然后根据hashCode值,判断元素在集合中的位置,每次存入元素都会比较hashCode值,
如果hashCode值相同,再去比较元素的值或者调用equals()方法判断是否相同,如果都相同,则将相同的元素替换,如果不同,以链表形式继续往后存放。
如果hashCode值不同,那么就存放在数组的不同位置。
将来,如果存放对象到set中,需要判断对象是否相同,那么要重写对象的equals()和hashCode方法
public class SetDemo01 {
public static void main(String[] args) {
//创建一个hashSet
/* HashSet<String> set = new HashSet<>();
//存放元素,无序存放,不能重复
set.add("hello");
set.add("hello");
set.add("JAVA");
set.add("WORLD");
set.add(null);
set.add(null);
System.out.println(set);
//遍历
for (String s : set) {
System.out.println(s);
}
System.out.println("----------------");
//iterator遍历
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
*/
HashSet<User> userSet = new HashSet<>();
userSet.add(new User(1001,"jack",20));
userSet.add(new User(1002,"tom",20));
userSet.add(new User(1003,"lucy",22));
userSet.add(new User(1001,"jack",20));
userSet.add(new User(1004,"tony",22));
System.out.println(userSet);
}
}
public class User {
private int id;
private String name;
private int age;
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public User() {
}
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 int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
if (id != user.id) return false;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
}
12.11.1 TreeSet
注释的说明:
实现了NavigableSet接口,有序的(自然排序),线程不安全,底层实际是一个TreeMap
构造方法
TreeSet()
构造一个新的,空的树组,根据其元素的自然排序进行排序。
TreeSet(Collection<? extends E> c)
构造一个包含指定集合中的元素的新树集,根据其元素的 自然排序进行排序 。
TreeSet(Comparator<? super E> comparator)
构造一个新的,空的树集,根据指定的比较器进行排序。
TreeSet(SortedSet<E> s)
构造一个包含相同元素的新树,并使用与指定排序集相同的顺序。
当我们在构造无参的TreeSet的时候,其实是在构造一个TreeMap对象
所以,treeSet中的元素的存放,就是按照TreeMap的键来实现存放的,treeMap的值也是做了默认的空对象常量的实现
将来如果需要存入对象到TreeSet的话,
1,这个对象要实现Comparable接口,从写compareTo()方法,实现对象的比较功能
2,在构造TreeSet对象的时候,传入Comparator对象
public class TreeSetDemo {
public static void main(String[] args) {
/* TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(4);
treeSet.add(1);
treeSet.add(3);
treeSet.add(5);
treeSet.add(3);
// treeSet.add(null); //不能存放null
System.out.println(treeSet);
System.out.println( treeSet.first());
System.out.println( treeSet.last());
//遍历
for (Integer integer : treeSet) {
System.out.println(integer);
}
Iterator<Integer> iterator = treeSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
*/
TreeSet<User> userSet = new TreeSet<>();
userSet.add(new User(1002,"jack",21));
userSet.add(new User(1001,"TOM",20));
userSet.add(new User(1004,"TONY",22));
userSet.add(new User(1003,"LUCY",19));
System.out.println(userSet);
}
}
public class User implements Comparable<User> {
private int id;
private String name;
private int age;
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public User() {
}
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 int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
if (id != user.id) return false;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + age;
return result;
}
@Override
public int compareTo(User o) {
if (o.id > this.id){
return 1;
}
if (o.id < this.id){
return -1;
}
if (o.age > this.age){
return -1;
}
if (o.age < this.age){
return 1;
}
if (o.name.compareTo(this.name) > 0){
return -1;
}
if (o.name.compareTo(this.name) > 0){
return 1;
}
return 0;
}
}
练习 :创建TreeSet的时候,传入Comparator,完成User对象的比较
TreeSet<User> userSet = new TreeSet<>(new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
if (o1.getId() > o2.getId()) {
return 1;
}
if (o1.getId() < o2.getId()) {
return -1;
}
if (o1.getAge() > o2.getAge()) {
return -1;
}
if (o1.getAge() < o2.getAge()) {
return 1;
}
if (o1.getName().compareTo(o2.getName()) > 0) {
return -1;
}
if (o1.getName().compareTo(o2.getName()) > 0) {
return 1;
}
return 0;
}
});
userSet.add(new User(1002,"jack",21));
userSet.add(new User(1001,"TOM",20));
userSet.add(new User(1004,"TONY",22));
userSet.add(new User(1003,"LUCY",19));
System.out.println(userSet);
12.11.2 LinkedHashSet
注释说明:
底层是哈希表+双向链表,就是hashMap+双向链表(底层可以看做是LinkedHashMap)
有序的,存取有序,允许存null,不能重复,线程不安全的
通过查看原码,发现LinkedHashSet中,只有几个构造方法,没有其他的普通方法,方法调用来自父类
构造方法中,都是通过super在访问父类,所以,创建LinkedHashSet,其实就是在通过HashSet创建HashMap对象
public class LinkedHashSetDemo {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("hello");
linkedHashSet.add("world");
linkedHashSet.add("java");
linkedHashSet.add("hello");
linkedHashSet.add("oracle");
linkedHashSet.add("abc");
System.out.println(linkedHashSet);
//遍历
for (String s : linkedHashSet) {
System.out.println(s);
}
Iterator<String> iterator = linkedHashSet.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
12.11.3 Set集合总结
HashSet : 无序、允许null,不能重复,底层是HashMap(数组+链表+红黑树),线程不安全
TreeSet :有序(自然排序),不允许null,不能重复,底层是TreeMap(红黑树),线程不安全
LinkedHashSet: 有序(存取有序),允许null,不能重复,底层是HashMap+双向链表,线程不安全