Java零基础入门7.0
- Java系列第七章
Java系列第七章
1. 游戏打包exe
游戏打包exe要考虑的因素:
游戏打包核心步骤:
2. API
2.1 Math
Math是一个帮助我们用于进行数学计算的工具类私有化构造方法,所有的方法都是静态的
在源码中的定义
public final class Math extends Object
即Math是一个最终类,不能被继承
Math是一个帮助计算的工具类 私有化构造方法,所有方法都是静态的
类Math包含用于执行基本数字运算的方法,例如基本指数,对数,平方根和三角函数
Math中的方法基本上都是静态修饰,可以直接通过类名调用
Math. 方法名()
例如:
Math.abs(-11)
常用方法:
abs的bug
以int为例
int类型的取值范围是-2147483648~2147483647
如果没有正数与负数对应,那么传递负数结果有误
解决方案
使用absExact
方法,当取值超过数据类型范围时,会有报错
System.out.println(Math.abs(88));//88
System.out.println(Math.abs(-88));//88
System.out.println(Math.absExact(-2147483648));//报错
ceil 向上取整
floor 去尾法
round 四舍五入
max 获取两个整数的最大值
min 获取两个整数的最小值
pow 获取a的b次幂
sqrt 开方 和 cbrt 开立方
random 获取随机数
2.2 练习
2.2.1 判断质数
但是这样遍历的话,如果要求的数非常大,那么就要从2开始依次遍历,非常麻烦
寻找因子的规律:
在左边因子当中,必定是小于等于该数(比如16)的平方根的
在右边因子当中,必定是大于等于该数(比如16)的平方根的
假设两个左右两边的数都是哪怕大于4的一点点,那么就不等于16
同理小于4的一点点时,也不等于
故其中一个因子当中一定是有小于等于平方根的,还有就是大于等于平方根的
以下因子情况:
所以判断一个数是否为质数的时候,不需要把所有的数全部遍历一遍,只需要把左边的因子数遍历判断就可以了
代码实现:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
System.out.println("请输入一个整数:");
Scanner sc = new Scanner(System.in);
int number = sc.nextInt();
System.out.println(isPrime(number));
}
public static boolean isPrime(int number) {
int count =0;
for (int i = 2; i <= Math.sqrt(number); i++) {
count++;
if (number % i == 0) {
return false;
}
}
System.out.println("循环了"+count+"次"+","+number+"是质数");
return true;
}
}
2.2.2 判断水仙花数(自幂数)
public class Main {
public static void main(String[] args) {
int count=0;
//得到每一个三位数
for (int i = 100; i < 999; i++) {
//个位 十位 百位
int ge =i%10;
int shi = i/10%10;
int bai = i/100%10;
//判断:
//每一位的三次方之和 跟本身进行比较
double sum = Math.pow(ge,3)+Math.pow(shi,3)+Math.pow(bai,3);
if(sum==i){
count++;
System.out.println(i);
}
}
System.out.println("循环了"+count+"次");
}
}
2.3 System
计算器机的时间原点:
currentTimeMillis()
来判断程序运行时间
long l = System.currentTimeMillis();
System.out.println(l);
//表示从开始到结束,采用了运行多少时间
arraycopy
数组拷贝
/*
第一个参数:数据源,表示被拷贝的数组
第二个参数:从数据原中的第几个索引开始拷贝
第三个参数:目的地,我要把数据拷贝到那个数组中
第四个参数:目的地数组的索引
第五个参数:拷贝的个数
*/
System.arraycopy(arr1,0,arr2,2,4);
for (int i = 0; i < arr2.length; i++) {
System.out.println(arr2[i]);
}
arraycopy的细节:
- 1.如果数据原数组和目的地数组的值都是基本数据类型,那么两者的类型必须保持一致,否则会报错
- 2.在拷贝的时候,需要考虑数组的长度,如果超出范围也会报错
- 3.如果数据原数组和目的地数组的值都是引用数据类型,那么,子类类型可以赋值给父类类型
//Person是Student的父类
Student s1=new Student("zhangsan",18);
Student s2=new Student("sanwu",18);
Student s3=new Student("lisan",18);
Student []arr1={s1,s2,s3};
Person []arr2=new Person[3];
//这里是可以赋值的,但在遍历arr2的时候,要把数据类型再进行转换,换成Student
System.arraycopy(arr1,0,arr2,0,3);
for(i=0;i<arr2.length.i++){
Student stu=(Student)arr2[i];
sout(stu.getname()+" "+stu.getage())
小结:
2.4 Runtime
Runtime表示当前虚拟机的运行环境
主要可以用来监视虚拟机的内存
这个类的方法不是静态的,需要先获取对象才能调用方法
1.获取Runtime的对象
//获取Runtime对象
//java的Runtime对象只有一个
Runtime r1=Runtime.getRuntime();
Runtime r2=Runtime.getRuntime();
System.out.println(r1==r2);
2.exit 停止虚拟机
这个实际上就是System.exit(0)的底层源码
Runtime.getRuntime().exit(0);
3.ailableProcessors 获取cpu线程数
System.out.println(Runtime.getRuntime().availableProcessors());
4.maxMemory 可以获得的总内存大小,单位byte字节
System.out.println(Runtime.getRuntime().maxMemory()/1024/1024);
5.totalMemory 已经获得的内存大小,单位byte字节
System.out.println(Runtime.getRuntime().totalMemory()/1024/1024);
6.freeMemory 虚拟机中剩余内存的大小
System.out.println(Runtime.getRuntime().freeMemory());
7. 运行cmd命令
//以字符串的形式运行cmd命令
//notepad,打开记事本
//shutdown,关机
//加上参数才能运行
//-s,默认一分钟之后关机
//-s -t 指定关机时间
//-a 取消关机操作
//-r 关机并重启
Runtime.getRuntime().exec("notepad");
2.5 Object
Object中没有成员变量,所以没有带参构造,只有空参构造方法
public Object
Object 的构造方法
2.5.1 Object 的成员方法
(1) toString
返回对象的字符串表现形式
public class Test1 {
public static void main(String[] args) {
Object obj = new Object();
String str1 = obj.toString();
System.out.println(str1);//java.lang.Object@4eec7777 @之前是包名+类名,@后是对象的地址值
sout(obj);//打印出来也是java.lang.Object@4eec7777
}
}
System.out.println(参数)
语句的解析
System是类名,out是一个静态变量,System.out:获取被打印的对象,println则是一个方法,参数就表示要打印的内容
核心逻辑:
当我们打印一个对象时,底层会调用对象的toString方法,把对象变成字符串,然后再打印在控制台上,打印完成后换行处理
将toString()打印地址值改成打印对象属性值的解决方案:
重写父类Object中的toString方法
toString方法的结论:
- 如果打印一个对象,想要看到属性值的话,那么在子类中重写toString方法就行可以了
- 在重写的方法中,把对象属性值进行拼接返回
@Override
public String toString() {
return name+","+age;
}
(2) equals 比较两个对象是否相等
原本比较的是地址值,通过重写(直接用alt+回车的重写)可以比较对象里的属性值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && name.equals(student.name);
}
//测试
Student stu1=new Student("zhangsan",18);
Student stu2=new Student("zhangsan",18);
// System.out.println(stu1);
boolean s1=stu1.equals(stu2);
System.out.println(s1);//结果为true
结论:
- 如果没有重写equals方法,那么默认使用Object中的方法进行比较,比较的是地址值是否相等
- 一般来讲地址值对我们意义不大,所以会进行方法的重写,将equals重写后比较的就是对象内部的属性值了
注:
String类中的equals方法会先判断参数是否为字符串,如果是字符串,再比较内部的属性,但是如果不是字符串,直接返回false
StringBuilder中没有equals方法,所以会直接使用Object中的equals方法,也就是比较地址值是否相等
(3) clone方法(Object对象中的方法)
clone()对象克隆
protected Object clone()
把A对象的属性值完全拷贝给B对象,也叫对象拷贝,对象复制
clone()源码,被protected修饰,所以不能直接调用,需要在子类里重写,
ctrl+鼠标点击
就可以查看java源码
Cloneable接口
- 如果一个接口中没有抽象方法
- 表示当前接口是一个标记性接口
- 当标记性接口(现在Cloneable表示)一旦实现,那么当前类的对象就可以被克隆
- 如果没有实现,当前类的对象就不能克隆 即,要想一个对象被克隆的话,就必须实现Cloneable这个接口,然后才能被克隆
alt + 回车
选择第二个异常签名,让克隆不报错
User类:
import java.util.StringJoiner;
public class User implements Cloneable{//实现额外的接口
private int id;//游戏角色
private String username;//用户名
private String password;//密码
private String path;//游戏图片
private int[] data;//游戏进度
public User() {
}
public User(int id, String username, String password, String path, int[] data) {
this.id = id;
this.username = username;
this.password = password;
this.path = path;
this.data = data;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int[] getData() {
return data;
}
public void setData(int[] data) {
this.data = data;
}
public String toString(){
return "角色编号为:"+id+",用户名 "+username+",密码"+password+",游戏图片"+path+",游戏进度"+arrToString();
}
public String arrToString(){
StringJoiner sj = new StringJoiner(",","[","]");
for (int i = 0; i < data.length; i++) {
sj.add(data[i]+"");
}
return sj.toString();
}
@Override
protected Object clone() throws CloneNotSupportedException {
//调用父类中的clone方法
//相当于让Java帮我们克隆一个对象,并把克隆之后的对象返回出去
return super.clone();
}
}
测试类:
public class ObjectDemo2 {
public static void main(String[] args) throws CloneNotSupportedException {
//1.先创建一个对象
int[] data ={1,2,3,4,5,6,7,8,9};
User u1 = new User(1,"maghua","12345","girl1",data);
//2.克隆对象,不能直接克隆
//因为当前方法是受保护的,表示当前的克隆方法只能被本包的类、还有其他包中的子类所使用
//又Object是定义再java.lang包下的,不能把代码写再lang包下
//所以,如果想要clone()方法,只能去User重写克隆方法
//细节:
//方法在底层会帮我们创建一个对象,并把原对象中的数据拷贝过去。
//书写细节:
//1.重写object中的clone方法
//2.让javabean类实现cloneable接口
//3.创建原对象并调用clone就可以了
User u2 = (User)u1.clone();//由于克隆出来的对象是用户对象,所以要进行强转
System.out.println(u1);
System.out.println(u2);
}
}
图解:
Cloneable接口:
package java.lang;
public interface Cloneable {
}
对象克隆方式一:
浅克隆(浅拷贝)
拷贝时,先创建一个新的对象,然后 基本数据类型就拷贝其数值, 引用数据类型就拷贝其地址值
深克隆(深拷贝)
拷贝时,也会先创建一个新的对象,然后 基本数据类型就拷贝其数值,
- 引用数据类型在拷贝时,如果是new出来的,就会在堆内存中开辟出一块新的空间,然后把之前老空间里的数据复制到新空间里,再把新空间的新地址值赋值给拷贝的变量,如果不是new出来的,像字符串,数据在串池里的,就会把之前的数据复用
实现代码:
上方User类的重写方法修改:
@Override
protected Object clone() throws CloneNotSupportedException {
//调用父类中的clone方法
//相当于让Java帮我们克隆一个对象,并把克隆之后的对象返回出去
//先把被克隆对象中的数组获取出来
int[] data = this.data;
//创建新的数组
int[] newData = new int[data.length];
//拷贝数组中的数据
for (int i = 0; i < data.length; i++) {
newData[i] = data[i];
}
//调用父类中的方法克隆对象
User u = (User)super.clone();
//因为父类中的克隆方法是浅克隆,替换克隆出来对象的数组地址值
u.data = newData;
return u;
}
测试类:
User u2 = (User)u1.clone();
int[] arr =u1.getData();
arr[0] = 100;
System.out.println(u1);
System.out.println(u2);
以后用到深克隆到实际项目当中时,就会使用第三方的一个工具
使用步骤:
- 1.先创建一个lib(Library)包,将第三方的工具导入当前项目中
- 2.编写代码
//先创建一个工具的对象
Gson gson=new Gson();
//把对象变成一个字符串
String s= gson.toJson(u1);
User user=gson.fromJson(s,User.class);
//打印对象
System.out.println(user);
这个深克隆的工具放到自己了解的路径当中
导入后,再点击添加到库中,如果有问题,看160集
小结:
即
- 浅克隆:不管对象内部的属性是基本数据类型还是引用数据类型,都完全拷贝过来
- 深克隆:基本数据类型拷贝过来,字符串复用,引用数据类型还会重新创建新的
总结:
2.5.2 Objects
objects是一个工具类,提供了一些方法去完成一些功能。
objects的成员方法:
Studnet s1 = null;
Stundent s2 = new Student("zhangsan",33);
boolean result=Objects.equals(s1,s2);
sout(Objects.isNull(s3));
sout(Objects.nonNull(s4));
直接使用Objects调用
细节:
- 1.Objects的equals方法的底层会先判断s1是否为null,如果时null,直接返回false
- 2.如果s1不为null,那么就利用s1再调用equals方法
- 3.此时的s1为Student类型,所以调用的还是Student的equals方法,如果没有重写,调用的就是Object的equals方法,比较地址值,如果重写了,就会比较属性值。
总结:
2.6 BigInteger(大的整数)
BigInteger(大的整数):
BigInteger可以表示超出long范围的整数,上限可以看作无限
2.6.1 Biglnteger构造方法:
(1) public BigInteger(int num,Random rnd) 获取随机大范围整数
public BigInteger(int num,Random rnd) 获取随机大范围整数
范围[0~2的num次方-1],包含0和2的num次方-1
Random r = new Random();
BigInteger bd1 = new BigInteger(4,r);
System.out.println(bd1);//2^4-1之内出现
(2) public BigInteger(String val) 获取指定的大整数
BigInteger bd2 = new BigInteger("999999999999999999999999999999999999999999999999999999999999999");//照样输出不会报错
BigInteger bd2 = new BigInteger("1.1");//有小数,直接报错
BigInteger bd2 = new BigInteger("abc");//字符串直接报错
(3) public BigInteger(String val,int radix) 获取指定进制的大整数
细节;
1.字符串中的数字必须是整数
2.字符串中的数字必须要跟进制吻合。
比如二进制中,那么只能写0和1,写其他的就报错。
BigInteger bd1=new BigInter("100",10);//这里获取的就是10进制的100,打印出来就是100
BigInteger bd2=new BigInter("100",2);//这里获取的就是2进制的100,打印出来就是4
BigInteger bd3=new BigInter("123",2);//这里代码就会报错,因为二进制中没有2和3
(4) BigInteger.valueOf(long val)
valueOf(long val),BigInteger的静态方法,可以获取BigInteger的对象,内部有优化
BigInteger(String val)与valueOf(long val)的不同之处
先来看相同的地方:他们都是创建一个大整数对象
BigInteger bd1=new BigInteger("100");
BigInteger bd1=BigInteger.valueOf(100);
不同:
1.valueOf能表示的范围比较小,只能在long的范围之内,不能超出long的范围
2.在内部,对常用的数字,-16到16进行了优化,提前把-16到16的BigInteger对象创建好了,多次获取时不会创建新的对象
3.如果超出了范围,就没有优化了,即使相同也会报错
BigInteger bd1=BigInteger.valueOf(16);
BigInteger bd2=BigInteger.valueOf(16);
sout(bd1==bd2);//==号比较的是地址值,结果是true,证明bd1和bd2是同一个对象
BigInteger bd3=BigInteger.valueOf(17);
BigInteger bd4=BigInteger.valueOf(17);
sout(bd3==bd4);//==号比较的是地址值,结果是false,证明bd3和bd4不是同一个对象.
- 如果不知道数据的范围有多大,就使用BigInteger(String val)
去获取,如果知道数据的范围在long范围内,就使用valueOf来获取 - 由于BigInteger对象一旦创建,内部记录的值就不能发生改变,所以,只要进行计算,像加减乘除在内的计算都会产生一个新的BigInteger对象
小结:
2.6.2 BigInteger常见成员方法
因为BigInteger是一个对象,不能直接相加减,要通过方法来完成
一些方法的使用:
import java.math.BigInteger;
public class BigIntegerTest {
public static void main(String[] args) {
BigInteger bd1=BigInteger.valueOf(15);
BigInteger bd2=BigInteger.valueOf(5);
//加
BigInteger bd3=bd1.add(bd2);
System.out.println(bd3);
//减
BigInteger bd4=bd1.subtract(bd2);
System.out.println(bd4);
//乘
BigInteger bd5=bd1.multiply(bd2);
System.out.println(bd5);
//除
BigInteger bd6=bd1.divide(bd2);
System.out.println(bd6);
//除,获取商的同时获取余数
//返回值是一个数组,第一个值记录商,第二个值记录余数
BigInteger[] bd7=bd1.divideAndRemainder(bd2);
System.out.println(bd7);
System.out.println(bd7[0]);
System.out.println(bd7[1]);
//比较是否相同
Boolean a=bd1.equals(bd2);
System.out.println(a);
//次幂
BigInteger bd9=bd1.pow(3);
BigInteger bd10=bd2.pow(3);
System.out.println(bd9+ " "+bd10);
//返回较大值
BigInteger bd11=bd1.max(bd2);
System.out.println(bd11);
//返回较小值
BigInteger bd12=bd1.min(bd2);
System.out.println(bd12);
//转换类型
int b=bd1.intValue();
System.out.println(b);
}
}
2.6.3 BigInteger底层存储方式
就是将一个大的整数分成多段存入数组中
1.先将数转化成补码
2.将补码32位分成一段,然后分成多段
3.再将多段补码转化为多段十进制整数
4.再将十进制整数存入数组
数组的最大长度就是int的最大值:2147483647(理论值)
总结:
2.7 BigDecima(大的小数)
计算机中的小数的存储方式
由于小数在计算机中用二进制表示时会很多,而每一种数据类型能够占用的空间是有限的,所以就会导致小数在计算机中的存储是不精确的
在实际应用中:
BigDecimal的作用:
- 用于小数的精确计算
- 用很大的小数
BigDecimal的一些构造方法:
方法名 | 说明 |
---|---|
public BigDecimal(double val) | 将 double 转换为 BigDecimal,但是,此构造方法的结果有一定的不可预知性,即还是有可能是不精确的,所以不推荐使用 |
public BigDecimal(String val) | 将字符串转换为 BigDecimal。,它不会遇到 BigDecimal(double) 构造方法的不可预知问题。 |
与BigInteger一样,BigDecimal也有valueOf(long val)
的静态方法,可以获取BigDecimal的对象,可以传递整数。
import java.math.BigDecimal;
public class BigDecimalTest {
public static void main(String[] args) {
//通过传递double类型的小数来创建对象
//这种方式有可能不精确,不推荐使用
BigDecimal bd1=new BigDecimal(0.01);
BigDecimal bd2=new BigDecimal(0.09);
System.out.println(bd1);
System.out.println(bd2);
//结果为
//0.01000000000000000020816681711721685132943093776702880859375
//0.0899999999999999966693309261245303787291049957275390625
//通过传递字符串表示的小数来创建对象
//常用的方式1
BigDecimal bd3=new BigDecimal("0.01");
BigDecimal bd4=new BigDecimal("0.09");
System.out.println(bd3);
System.out.println(bd4);
//结果为
//0.01
//0.09
//通过静态方法获取对象
BigDecimal bd6 = BigDecimal.valueOf(10);
BigDecimal bd7 = BigDecimal.valueOf(10);
System.out.println(bd6 == bd7);//true
BigDecimal bd6 = BigDecimal.valueOf(10.0);
BigDecimal bd7 = BigDecimal.valueOf(10.0);
System.out.println(bd6 == bd7);//false
}
}
通过传递字符串表示的小数来创建对象与valueOf静态方法
的差别
- 如果表示的数字没有超出double的取值范围,推荐使用静态方法
- 如果要表示的数字比较大,超出了double的取值范围,建议使用构造方法
- 在静态方法中,如果传递的是0~10之间的整数(包括0和10),那么,方法会返回已经创建好的对象,不会重新new,跟BigInteger是一样的原理,但是,如果传递的是小数的话,就不会有优化,会创建新的对象
2.7.1 Bigdecimal的常见方法
//前面几个跟BigInteger没什么差别
//主要是除法
BigDecimal bd1 = BigDecimal.valueOf(10.0);
BigDecimal bd2 = BigDecimal.valueOf(3.0);
//除法
BigDecimal bd6 = bd1.divide(bd2);
System.out.println(bd6);//报错
//当除不尽时,public BigDecimal divide(BigDecimal val)这个方法就会报错,
//此时,就要使用
//public BigDecimal divide(BigDecimal val,精确几位,舍入模式)。这个方法
BigDecimal bd6 = bd1.divide(bd2,2,BigDecimal.ROUND_HALF_UP);
System.out.println(bd6);//结果为3.33
其他的舍入模式可以在api文档中的RoundingMode
类中可以找到:
数轴表示:
2.7.2 Bigdecimal的底层存储方式
Bigdecimal会先得到字符串里的字符,将字符串分成一个个单独的字符,在将字符转化为ascll码表上的数值,再存储到数组里。
例如:
总结:
2.7 正则表达式
正则表达式用于校验字符串的相关操作,可以很方便
字符串通过使用matches
方法来匹配相应的正则表达式
"abc".matches("[bcd]");//结果为false
练习:
public static void main(String[] args) {
//规则:6位及20位之内,0不能在开头,必须全部是数字
//一个编程思想:
// 先把异常数据进行过滤,
// 过滤后得到的就是满足要求的数据了
String qq = "1234567890";
//System.out.println(checkQQ(qq));但是这样写就稍微麻烦了
System.out.println(qq.matches("[1-9]\\d{5,19}"));//[5,19]表示6位到20位
}
public static boolean checkQQ(String qq){
int len =qq.length();
if(len<6||len>20){
return false;
}
//0不能在开头
if(qq.startsWith("0")){
return false;
}
//必须全部是数字
for (int i = 0; i < qq.length(); i++) {
char c = qq.charAt(i);
if(c<'0'||c>'9'){
return false;
}
}
return true;
}
}
正则表达式的作用:
- 1.校验字符串是否满足规则
- 2.在一段文本中查找满足要求的内容
2.7.1 正则表达式的字符
上面的方括号([ ])
在正则表达式中表示一个字符的范围,字符串里出现的字符一定要在这个范围之内,不在范围之内的话会返回false
^
在正则表达式中表示取反的意思&&
在正则表达式中表示取交集的意思()
是分组|
在方括号外表示并集
在java中,\
表示转义字符,改变后面那个字符原本的含义
sout(" \" ")
打印出来的就是,sout(" " ")
这样写会报错
\\
就表示一个普通的\
符号,不表示转义字符
1.字符类例子:
取值判断
sout("a".metches("[abc]"));//true
sout("ab".metches("[abc]"));//false,因为这里的正则表达式只能表示一个字符的范围
//,如果想表示两个字符的范围的话,可以这样写
sout("ab".metches("[abc][abc]"));//这样才可以表示两个字符
取反
sout("a".metches("[^abc]");//false
sout("d".metches("[^abc]");//true
sout("zz".metches("[^abc]"));//false
sout("zz".metches("[^abc][^abc]"));//false
a-z A-Z(包括头尾的范围)
sout("a".metches("[a-zA-Z]");//true
sout("z".metches("[a-zA-Z]");//true
sout("aa".metches("[a-zA-Z]");//false
sout("zz".metches("[a-zA-Z]");//false
sout("zz".metches("[a-zA-Z][a-zA-Z]");//true
sout("0".metches("[a-zA-Z0-9]");//ture
[a-d[m-p]]: a到d,或m到p
sout("a".metches("[a-d[m-p]]");//true
sout("d".metches("[a-d[m-p]]");//true
sout("m".metches("[a-d[m-p]]");//true
sout("p".metches("[a-d[m-p]]");//true
sout("e".metches("[a-d[m-p]]");//false
[a-z&&[def]]: a-z和def的交集。为: d, e, f
//如果要求两个范围的交集,那么需要写符号&&
//这里如果只写了一个&,那么就不表示为一个交集了,就是单纯的一个&符号,没有任何含义
sout("a".metches("[a-z&&[def]]");//false
sout("a".metches("[a-z&[def]]");//true
sout("d".metches("[a-z&&[def]]");//true
sout("&".metches("[a-z&[def]]");//true
sout("0".metches("[a-z&[def]]");//false
[a-z&&[^bc]:a-z和非bc的交集。(等同于[ad-z])
sout("a".metches("[a-z&&[^bc]");//ture
sout("b".metches("[a-z&&[^bc]");//false
sout("0".metches("[a-z&&[^bc]");//false
[a-z&&[^m-p]:a到z和除了m到p的交集。(等同于[a-lq-z])
sout("a".metches("[a-z&&[^m-p]");//true
sout("m".metches("[a-z&&[^m-p]");//false
sout("0".metches("[a-z&&[^m-p]");//false
2.预定义字符类:
.表示任意一个字符
sout("你".metches(".."))//false,因为有两个. ,所以需要两个字符
sout("你是".metches(".."))//true,
sout("你".metches("."))//true
\\d
因为\是转义字符,所以用\\来表示一个普通的\,则\\d就表示\d,而\d表示任意的一个数字
// 简单来记: 两个\表示一个\
sout("0".metches("\\d"))//true
sout("n".metches("\\d"))//false
sout("21".metches("\\d"))//false
sout("21".metches("\\d\\d"))//true
\\D
sout("n".metches("\\D"))//true
sout("1".metches("\\D"))//false
\\w 只能是一位单词字符[a-zA-Z_0-9]
sout("n".metches("\\w"))//true
sout("_".metches("\\w"))//true
sout("8".metches("\\w"))//true
sout("你".metches("\\w"))//false
\\W 非单词字符
sout("n".metches("\\W"))//false
sout("_".metches("\\W"))//false
sout("0".metches("\\W"))//false
sout("你".metches("\\W"))//true
2.7.2 正则表达式的数量词
正则表达式的数量词,可以一次表示多个字符
// x{6,} 至少出现6位
sout("234jnhs".matches("\\w{6,}"));//true
sout("23sx".matches("\\w{6,}"));//false
//x{4}必须是4位
sout("3424".matches("[a-zA-Z0-9]{4}"))//true
sout("3A424".matches("[a-zA-Z0-9]{4}"))//false
忘记的可以看api文档Pattern
类
2.7.3 正则表达式练习
一、
public static void main(String[] args) {
//细节:
//拿着一个正确的数据,从做到右依次去写
//13112345678
//分成三部分:
//第一部分:1表示手机号码只能以1开头
//第二部分:[3-9]表示手机号码第二位只能是3-9之间的
//第三部分: \\d{9}表示任意数字可以出现9次,也只能出现9次
String regex1 = "1[3-9]\\d{9}";
System.out.println("13112345678".matches(regex1));
System.out.println("131".matches(regex1));
System.out.println("------------------------------------------");
//座机号码
//020-2324242 02122442 027-42424 0712-3242434
//思路:
//在书写座机号正则的时候需要把正确的数据分为三部分
//一:区号 0\\d{2,3}
// 0:表示区号一定是以0开头的
// \\d{2,3}:表示区号从第二位开始可以是任意的数字,可以出现2到3次
//二:- ?表示次数,0或一次
//三:号码 号码的第一位也不能以0开头,从第二位开始可以是任意的数字,号码的总长度:5-10位
String regex2 = "0\\d{2,3}-?[1-9]\\d{4,9}";
System.out.println("020-2324242".matches(regex2));
System.out.println("0712-3242434".matches(regex2));
System.out.println("------------------------------------------");
//邮箱号码
//3232323@qq.com zhangsan@itcast.cnn dleieee9@163.com dleieees@pci.com.cn
//思路:
//在书写邮箱号码正则的时候需要把正确的数据分为三部分
//第一部分:@的左边 \\w+,一个非单词字符串出现多次
//任意的字母数字下划线,至少出现一次就可以了
//第二部分:@ 只能出现一次
//第三部分: . 的左边可以是字符也可以是数字 [\\w&&[^_]]{2,6}
// 任意的字母加数字,总共出现2-6次(此时不能出现下划线)
// . \\.
// 大写字母,小写字母都可以,只能出现2-3次[a-zA-Z]{2,3)
String regex3 = "\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}){1,2}";
System.out.println("3232323@qq.com".matches(regex3));
System.out.println("dleieees@pci.com.cn".matches(regex3));
}
在实际开发中,使用anyrule插件可以快捷创建正则表达式
右键点击
根据时间需求,把开头和末尾的符号删掉
String regex4="(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d";
以:
作为区分
//24小时的正则表达式 23:11:11
String regex4 = "([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d";
System.out.println("23:11:11".matches(regex4));//true
//优化
String regex5 = "([01]\\d|2[0-3])(:[0-5]\\d){2}";
System.out.println("23:11:11".matches(regex5));//true
二、
public static void main(String[] args) {
//用户名要求:大小写字母,数字,下划线一共4-16位
String regex1 = "\\w{4,16}";
System.out.println("magua".matches(regex1));
System.out.println("------------------------------------------");
//身份证号码的简单校验:
//18位,前17位任意数字,最后一位可以是数字可以是大写或小写的x
String regex2 = "[1-9]\\d{16}(\\d|X|x)";
System.out.println("41080119930228457x".matches(regex2));
//另类写法方式一
String regex3 = "[1-9]\\d{16}[\\dXx]";
System.out.println("510881197609822309".matches(regex3));
System.out.println("------------------------------------------");
//忽略大小写的书写方式
//在匹配的时候忽略abc的大小写
//方式二
String regex4 = "a((?i)b)c";//表示只忽略a后面b的大小写
System.out.println("abc".matches(regex4));//true
System.out.println("ABC".matches(regex4));
System.out.println("aBc".matches(regex4));
System.out.println("------------------------------------------");
//方式三
String regex5 = "[1-9]\\d{16}(\\d|(?i)x)";
System.out.println("15040119810705387X".matches(regex5));
System.out.println("------------------------------------------");
//身份证号码的严格校验
//4108011993 02 28 457x
//前面6位:省份,市区,派出所等信息﹑第一位不能是0,后面5位是任意数字 [1-9]\\d{5}
//年的前半段:18 19 20 (18|19|20)
//年的后半段:任意数字出现两次 \\d{2}
//月份: 01 ~ 09 10 11 12 (0[1-9]|1[0-2])
//日期: 01 ~ 31 10~19 20~29 30 31 (0[1-9]|[12]\\d|3[01])
//后面四位:
//任意数字出现3次最后一位可以是数字也可以是大写x或者小写x \\d{3}[\\dXx]
//编写正则的小心得:
//第一步:按照正确的数据进行拆分
//第二步:找每一部分的规律,并编写正则表达式
//第三步:把每一部分的正则拼接在一起,就是最终的结果书写的时候:从左到右去书写。
String regex6 = "[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]";
String regex7 = "[1-9]\\d{5}(18|19|20)\\d{2}(0\\d|10|11|12)(0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]";
System.out.println("41080119930228457x".matches(regex6));
System.out.println("41080119930228457x".matches(regex7));
}
小结(一些符号的作用):
2.7.4 爬虫
爬虫即在一段文本中查找满足的内容
使用的两个类:
- Pattern:表示正则表达式
获取正则表达式的对象:
通过Pattern的静态方法complie获取正则表达式的对象
Pattern p=Pattern.compile(“Java\d{0,2}”);
- Matcher:文本匹配器,作用按照正则表达式的规则去读取字符串,从头开始读取,在大字符串中去找符合匹配规则的字串
1.通过Matcher获取文本匹配器的对象
Matcher m=p.matcher(str);
(用m读取str,按照p的规则找里面的小串)2.通过循环获取每一个满足条件的值,循环里通过Matcher的group方法获取具体字符串,循环的条件是由Matcher对象的find方法来判断循环是否停止
练习1:
ctrl + alt + m 快捷键 选中内容快捷包含生成一个方法
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public static void main(String[] args) {
String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,"+
"因为这两个是长期支持版木,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";
//获取正则表达式的对象
Pattern p = Pattern.compile("Java\\d{0,2}");
//通过Pattern的静态方法complie获取正则表达式的对象
//获取文本匹配器的对象
//m:文本匹配器的对象
//str:大串
//p:规则
//解释:m要在str中找符合p规则的小串
Matcher m = p.matcher(str);
//拿着文本匹配器从头开始读取,寻找是否有满足规则的子串
//如果没有,方法返回false
//如果有,返回true,在底层记录字串的起始索引喝结束索引+1
//0,4
boolean b = m.find();
//方法底层会根据find方法记录的索引进行字符串的截取
//subString(起始索引,结束索引);包头不包尾
//(0,4)但是不包含4索引
//会把截取的小串进行返回
String s1 = m.group();
System.out.println(s1);
//第二次在调用find的时候,会继续读取后面的内容
//读取到第二个满足要求的字串,方法会继续返回true
//并把第二个子串的起始索引和结束索引+1,进行记录
b = m.find();
//第二次调用group方法的时候,会根据find方法记录的索引再次截取子串
String s2 = m.group();
System.out.println(s2
);
}
但是,这样写有个弊端,就是不能找到所有符合p正则条件的
修改:
String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11,"+
"因为这两个是长期支持版木,下一个长期支持版本是Java17,相信在未来不久Java17也会逐渐登上历史舞台";
//method1(str);
//1.获取正则表达式的对象
Pattern p = Pattern.compile("Java\\d{0,2}");
//2.获取文本匹配器的对象
//拿着m去读取str,找到符合p规则的字符
Matcher m = p.matcher(str);
//3.利用循环获取所以符合p规则的
while(m.find()){
String s = m.group();
System.out.println(s);
}
练习2:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexDemo1 {
public static void main(String[] args) {
/*
手机号正则表达式:1[3-9]\d{9}
邮箱正则表达式:\w+@[\w&&[^_]]{2,6}(\.[a-zA-Z]{2,3}}){1,2}
座机电话表达式:0\d{2,3}-?[1-9]\d{4,9}
热线电话表达式:400-?[1-9]\\d{2}-?[1-9]\\d{3}
*/
String s ="来黑马程序员学习Java, "+
"电话:18512516758,18512508907"+
"或者联系邮箱: boniu@itcast.cn, " +
"座机电话: 01036517895,010-98951256"+
"邮箱: bozai@itcast.cn," +
"热线电话: 400-618-9090 , 400-618-4080,4006184008,4006189090";
String regex = "(1[3-9]\\d{9})" +
"|(\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3}}){1,2})" +
"|(0\\d{2,3}-?[1-9]\\d{4,9})" +
"|(400-?[1-9]\\d{2}-?[1-9]\\d{3})";
//1.获取正则表达式的对象
Pattern p = Pattern.compile(regex);
//2.获取文本匹配器的对象
//利用m去读取s,会按照p的规则找里面的小串
Matcher m = p.matcher(s);
//3.利用循环获取每一个数据
while (m.find()){
String str = m.group();
System.out.println(str);
}
}
}
一、带条件的数据爬取
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
String s="Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11," +
"因为这两个是长期支持版本,下一个长期支持版木是Java17,相信在未来不久Java17也会逐渐登上历史舞台";
//1.定义正则表达式
//?理解位前面的数据Java
//=表示在Java后面要跟随的数据
//但是在获取的时候,只获取前半部分
//需求1:
String regex1 = "((?i)Java)(?=8|11|17)";
//需求2:
String regex2 = "((?i)Java)(8|11|17)";
String regex3 = "((?i)Java)(?:8|11|17)";//?:同样表示2的效果,选取中有的
//需求3:
String regex4 = "((?i)Java)(?!8|11|17)";
Pattern p = Pattern.compile(regex1);
Matcher m = p.matcher(s);
while (m.find()){
System.out.println(m.group());
}
}
}
二、贪婪爬取和非贪婪爬取
- 贪婪爬取:在爬取时,尽可能多的爬取数据
- 非贪婪爬取:尽可能少爬取数据
Java默认为贪婪爬取
如果我们在数量词+
*
后加上问号,那么此时就是非贪婪爬取
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
String s="Java自从95年问世以来,abbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaa" +
"经历了很多版本,目前企业中用的最多的是Java8和Java11," +
"因为这两个是长期支持版本,下一个长期支持版木是Java17,相信在未来不久Java17也会逐渐登上历史舞台";
String regex1 = "ab+?";
String regex2 = "ab+";
Pattern p1 = Pattern.compile(regex1);
Matcher m1 = p1.matcher(s);
Pattern p2 = Pattern.compile(regex2);
Matcher m2 = p2.matcher(s);
while(m1.find()){
System.out.println(m1.group());
}
while(m2.find()){
System.out.println(m2.group());
}
}
}
2.7.5 正则表达式在字符串方法中的使用
如果说字符串方法的形参名字为regex,那么就能识别正则表达式
练习:
public static void main(String[] args) {
String s="小实时jasndjasnoid1231小淡淡ndasjkldnan213131小灰灰";
//细节:
//方法在底层跟之前一样也会创建文本解析器的对象
//然后从头开始去读取字符串的内容,只要有满足的,那么久用第二个参数去替换
String result = s.replaceAll("[\\w&&[^_]]+","vs");
System.out.println(result);
String[] arr = s.split("[\\w&&[^_]]+");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
2.7.6 分组
分组就是一个小括号
与数学的计算相似
在正则表达式中,\\x(组号)
就表示把第x组的数据再拿出来用一次
每组是有组号的 ,也就是序号
- 规则1;从1开始,连续不间断
- 规则2;
以左括号为基准,最左边的是第一组,其次为第二组,依次类推
(1)捕获分组
练习1:
public static void main(String[] args) {
//a123a b2324b c1234c d2131a(false)
String regex1="(.).+\\1";
// (.)表示首字符可以是任意的
// .+表示在该中间是任意的
// \\1 表示把第1组的内容再拿出来用一次,也就是(.)里面的首字母,再后面也必须要相同
System.out.println("a123a".matches(regex1));
System.out.println("b2324b".matches(regex1));
System.out.println("c1234c".matches(regex1));
System.out.println("d2131a".matches(regex1));
System.out.println("——————————————————————————————————————————————————");
//abc213abc
String regex2="(.+).+\\1";
// (.+)表示任意一个字符至少出现一次
System.out.println("abc123abc".matches(regex2));
System.out.println("bc2324bc".matches(regex2));
System.out.println("csx1234csx".matches(regex2));
System.out.println("dz2131ac".matches(regex2));
System.out.println("——————————————————————————————————————————————————");
//aaa123aaa
//括起来两个括号之后,表示两组,\\1 就要变成 \\2了
//这里的\\2:把第2组再拿出来再使用
// *: 作用于\\2,表示是后面重复的内容出现0次或多次
// 大括号的一个整体,后面的\\1表示拿出来一个整体用
String regex3="((.)\\2*).+\\1";
System.out.println("aaa123aaa".matches(regex3));
System.out.println("aaa123aab".matches(regex3));
}
练习2:
public static void main(String[] args) {
String str="我要学学编编编编编编编编程程程程程程程程程程程";
//需求:把重复的内容替换为单个的学学
//学学 学
//编编编编 编
//程程程程程程 程
//(.) 表示把重复内容的第一个字符看作第一组
// \\1 表示第一个字符再次出现
// + 表示至少一次
// $1 表示把正则表达式中第一组的内容,再拿出来用
String result = str.replaceAll("(.)\\1+","$1");
System.out.println(result);
}
(2)非捕获分组
分组之后不需要再用本组数据,仅仅是把数据括起来,不占用组号,不能用\\
组号来使用非捕获
上述表格就的具体案例就在带条件的数据爬取
小结:
2.8 JDK7时间
JDK7时间有三个类
Date:时间类
SimpleDateFormat: 格式化时间类
Calender: 日历类
2.8.1 Date 时间
时间的相关知识
Date的常用方法:
方法名 | 作用 |
---|---|
public Date() | 创建Date对象,表示当前时间 |
public Date(long date) | 创建Date对象,表示指定时间 |
public void setTime(long time) | 设置/修改毫秒值 |
public long getTime | 获取时间对象的毫秒值 |
展示:
注意导包一定要点java.lang的
public static void main(String[] args) {
//1.创建对象表示一个时间
Date d1 = new Date();
System.out.println(d1);
//2.创建对象表示一个指定的时间
Date d2 = new Date(0L);//对于long类型的,可以加个L
System.out.println(d2);//表示从时间原点开始过了0毫秒
//3.setTime 修改时间
d2.setTime(1000L);
System.out.println(d2);//在时间原点上过了1000毫秒,也就是1秒
//4.getTime获取当前时间的毫秒值
long time = d2.getTime();//在左边基础上 ctrl + alt + v
System.out.println(time);
}
练习:
public static void main(String[] args) {
Random r=new Random();
//创建两个时间对象
Date d2=new Date(Math.abs(r.nextInt()));
Date d3=new Date(Math.abs(r.nextInt()));
long time1 = d2.getTime();
long time2 = d3.getTime();
if(time1>time2){
System.out.println("第一个时间在后面,第二个时间在前面");
}else if(d2.getTime()< d3.getTime()){
System.out.println("第二个时间在后面,第一个时间在前面");
}else{
System.out.println("表示两个时间一样");
}
method1();
}
private static void method1() {
//1.创建一个对象,表示时间原点
Date d1=new Date(0L);
//2.获取的时间的毫秒值
long time=d1.getTime();
//3.在这个基础上我们要加一年的毫秒值即可
time=time+1000L*60*60*24*365;
//4.把计算之后的时间毫秒值,再设置回d1当中
d1.setTime(time);
System.out.println(d1);
}
小结:
2.8.2 SimpleDateFormat
由于Date类只能按照默认方法取表示,所以出现了SimpleDateFormat类
格式化时间格式的常用模式关系:
在api帮助文档里有:
1.格式化案例代码:
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateDemo1 {
public static void main(String[] args) {
//1.利用空参构造创建SimpleDateFormat对象,默认格式
SimpleDateFormat sdf1 = new SimpleDateFormat();
Date d1 = new Date(0L);
String str1 = sdf1.format(d1);//format()表示格式化的意思,里面的参数要是Date对象
System.out.println(str1);
//2.利用带参构造创建SimpleDateFormat对象,指定格式
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EE");
String str2 = sdf2.format(d1);
System.out.println(str2);
}
}
2.解析案例代码:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateDemo1 {
public static void main(String[] args) throws ParseException {
//1.定义一个字符串表示时间
String str = "2023-11-11 11:11:11";
//2.利用空参构造创建simpleDateFormat对象
// 细节:
//创建对象的格式要跟字符串的格式完全一致
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(str);//注意alt + 回车 处理异常
System.out.println(date);
System.out.println(date.getTime());
}
}
练习1:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateDemo1 {
public static void main(String[] args) throws ParseException {
String str = "2000-11-11";
//2.解析
SimpleDateFormat sdf1 = new SimpleDateFormat("yyy-MM-dd");
Date date = sdf1.parse(str);
String result2 = sdf1.format(date);
System.out.println(result2);
//3.格式化
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日");
String result = sdf2.format(date);
System.out.println(result);
}
}
练习2:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateDemo1 {
public static void main(String[] args) throws ParseException {
//1.定义字符串表示三个时间
String startStr = "2023年11月11日 0:0:0";
String endStr = "2023年11月11日 0:10:0";
String orderStr = "2023年11月11日 0:01:00";
//2.解析上面的三个时间,得到Date对象
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date startDate = sdf.parse(startStr);
Date endDate = sdf.parse(endStr);
Date orderDate = sdf.parse(orderStr);
//3.得到三个时间的毫秒值
long startTime = startDate.getTime();
long endTime = endDate.getTime();
long orderTime = orderDate.getTime();
if(orderTime>=startTime&&orderTime <= endTime){
System.out.println("秒杀成功");
}else{
System.out.println("秒杀失败");
}
}
小结:
2.8.3 Calendar
对于时间转化来说,这样的步骤就稍微麻烦了点
Calendar表示了系统当前时间的日历对象,可以单独修改,获取时间中的年,月,日。
应用场景:可以方便的修改时间
Calendar的常用方法:
在日历对象中,月份的范围是0-11,所以要在获取的月份值上+1,才表示真正的月份
星期的值是1,2,3,4,5,6,7,但是,在老外眼里,星期日是一周的开始, 所以1在日历对象中代表的是周日
即1(周日)2(周一)3(周二)4(周三)5(周四)6(周五)7(周六)
例子:
import java.util.Calendar;
import java.util.Date;
public class DateDemo1 {
public static void main(String[] args) {
//Calendar是一个抽象类,不能直接new,而是通过一个静态方法获得子类对象
//底层原理:
//会根据系统的不同时区返回不同日历对象,默认表示当前时间
//会把年,月,日,时,分,秒等都放到Calendar对象的一个数组中
//细节2:
//月份:范围8~11如果获取出来的是0.那么实际上是1月。星期:在老外的眼里,星期日是一周中的第一天
//1(星期日) 2(星期一) 3(星期二) 4(星期三) 5(星期四) 6(星期五) 7(星期六)
Calendar c=Calendar.getInstance();
/*[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,
transitions=31,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,
ERA=1,YEAR=2023,MONTH=3,WEEK_OF_YEAR=15,WEEK_OF_MONTH=3,DAY_OF_MONTH=9,DAY_OF_YEAR=99,
DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=3,HOUR_OF_DAY=15,MINUTE=6,SECOND=44
,MILLISECOND=103,ZONE_OFFSET=28800000,DST_OFFSET=0]
*/
Date d=new Date(0L);
c.setTime(d);
System.out.println(c);
//public int get(int field) 取日期中的某个字段信息
//public void set(int field,int value) 修改日历的某个字段信息
//public void add(int field,int amount) 为某个字段增加/减少指定的值
c.set(Calendar.YEAR,2000);//修改年份
c.set(Calendar.MONTH,8);//修改月份
c.set(Calendar.DAY_OF_MONTH,10);//修改属于该月份的天数
//调用方法在这个基础上增加一个月
c.add(Calendar.MONTH,-1);
//会把年,月,日,时,分,秒等都放到Calendar对象的一个数组中
//数组索引分别代表
//0:纪元
//1:年
//2:月
//3:一年中的第几周
//4:一个月中的第几周
//5:一个月中的第几天
//。。。以此类推,一直到16.
int year = c.get(1);
int month = c.get(2)+1;
int date = c.get(5);
System.out.println(year+","+month+","+date);
//但是这样写数字,可阅读性太低了
int year1=c.get(Calendar.YEAR);//等同于int year=c.get(1)
int month1=c.get(Calendar.MONTH)+1;
int date1 = c.get(Calendar.DAY_OF_MONTH);
int week1 =c.get(Calendar.DAY_OF_WEEK);
System.out.println(year1+" "+month1+" "+date1+" "+week1);
//在日历对象中,月份的范围是0-11,所以要在获取的月份值上+1,才表示真正的月份
//getTime
System.out.println(getWeek(week1));
}
//查表法
//表:容器
//让数据跟索引产生对应的关系
//传入对应的数字: 1~7
//返回对应的星期
public static String getWeek(int index){
//定义一个数组,让汉字星期几,跟1~7产生对应关系
String[]arr={"","周日","周一","周二","周三","周四","周五","周六"};
//根据索引返回对应的星期
return arr[index];
}
}
小结:
2.9 JDK8 新增时间类
jdk7的麻烦:
为何增加JDK8的时间相关类
- 1.jdk7的代码较为麻烦,jdk8的代码简单
- 2.jdk7在多线程环境下可能会导致数据安全的问题,jdk8的时间日期对象都是不可变的,解决了这个安全问题
新用法:
对比:
JDK8新增时间相关类:
分类表示:
2.9.1 Date类(ZoneId,Instant,ZoneDateTime)
一、ZoneId 时区
import java.time.ZoneId;
import java.util.Set;
public class DateDemo1 {
public static void main(String[] args) {
//1.获取所有的时区名称
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
System.out.println(zoneIds.size());//602
System.out.println(zoneIds);//所有时区
//2.获取当前系统的默认时区
ZoneId zoneId = ZoneId.systemDefault();
System.out.println(zoneId);
//3.获取指定的时区
ZoneId zoneId1 = ZoneId.of("Asia/Shanghai");
System.out.println(zoneId1);
}
}
二、Instant (时间戳)
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class DateDemo1 {
public static void main(String[] args) {
//1.获取当前时间的Instant的对象(标准时间)
Instant now = Instant.now();
System.out.println(now);
//2.根据(秒/毫秒/纳秒)获取Instant对象
//ofXxx方法
//ofEpochMilli 获取的是指定毫秒值的时间戳
Instant instant1 = Instant.ofEpochMilli(0L);
System.out.println(instant1);//1970-01-01T00:00:00Z
//ofEpochMilli 获取的是指定秒值的时间戳
Instant instant2 = Instant.ofEpochMilli(1L);
System.out.println(instant2);//1970-01-01T00:00:00.001Z
//ofEpochMilli 获取的是指定秒加上纳秒值的时间戳
Instant instant3 = Instant.ofEpochSecond(1L,100000000L);
System.out.println(instant3);//1970-01-01T00:00:01.100Z
//3.指定时区
//atZone方法
//这里atZone的参数是ZoneId的对象
ZonedDateTime time = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(time);//2023-04-17T20:31:33.236025200+08:00[Asia/Shanghai]
//4.isXxx
Instant it4=Instant.ofEpochMilli(0L);
Instant it5=Instant.ofEpochMilli(1000L);
//5.用于时间的判断
//isBefore:判断调用者时间是否在参数时间的前面
boolean rs1=it4.isBefore(it5);
System.out.println(rs1);//true
//isAfter:判断调用者时间是否在参数时间的后面
boolean rs2=it4.isAfter(it5);
System.out.println(rs2);//false
//6. minusXxx:减少时间系列的方法
Instant it6=Instant.ofEpochMilli(3000L);
System.out.println(it6.minusMillis(3000L));//minusMillis:减少毫秒
System.out.println(it6.minusSeconds(3L));//minusMillis:减少秒
System.out.println(it6.minusNanos(3000000000L));//minusMillis:减少纳秒
//同理,plusXxx与minusXxx方法相反
}
}
三、ZoneDateTime类(带时区的时间)
ofXxxx()有两种方法可以获取指定时间对象:
- 方法1: 通过指定年月日时分秒纳秒+时区方式指定
ZoneDateTime time=ZoneDateTime.of(2023,4,7,16,13
,11,0,ZoneId.of("Asia/Shanghai"));
sout(time);
- 方法2
通过Instant+时区的方式指定
Instant it=Instant.ofEpochMilli(0L);
ZoneId zoneid=ZoneId.of("Asia/Shanghai"));
ZoneDateTime time=ZoneDateTime.ofInstant(it,zoneid);
sout(time);
其余方法都是创建完ZoneDateTime对象后直接调用就行,ieda会有提示
用法跟Instant相似
注:JDK8新增的时间对象都是不可变的 如果修改了一个时间后,调用者不会发生改变,但会产生一个新的时间
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class DateDemo1 {
public static void main(String[] args) {
//1.获取当前时间对象(带时区)
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);
//2.获取指定的时间对象(带时区)
//年月日时分秒纳秒方式指定
ZonedDateTime time1 = ZonedDateTime.of(2023,10,1,
11,12,12,0,
ZoneId.of("Asia/Shanghai"));
System.out.println(time1);
//通过Instant+时区的方式指定获取时间对象
Instant instant = Instant.ofEpochMilli(0L);
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
ZonedDateTime time2 = ZonedDateTime.ofInstant(instant,zoneId);
System.out.println(time2);
//3.withXxx 修改时间系列的方式
ZonedDateTime time3 = time2.withYear(2000);
System.out.println(time3);
//4.减少时间
ZonedDateTime time4 = time3.minusYears(1);
System.out.println(time4);
//5.增加时间
ZonedDateTime time5 = time4.plusYears(1);
System.out.println(time5);
//细节:
//JDK8新增的时间对象都是不可变的
// 如果我们修改了,减少了,增加了时间
//那么调用者是不会发生改变的,产生一个新的时间
}
}
2.9.2 SimpleDateFormat类(日期格式化类)(DateTimeFormatter)
- 第一个方法会指定时间的格式,返回DateTimeFormatter对象
- 调用第二个方法会按照指定的格式返回相应的字符串
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class DateDemo1 {
public static void main(String[] args) {
//获取时间对象
ZonedDateTime z1=ZonedDateTime.now();
System.out.println(z1);
//解析格式化器
DateTimeFormatter d1=DateTimeFormatter.ofPattern("yyyy--MM--dd HH::mm::ss EE a");
System.out.println(d1.format(z1));
}
}
2.9.3 Calendar(LocalDate 、 LocalTime、LocalDateTime)
注:LocalDate和LocalTime用get开头的方法时,只能获得相对应的信息
即:LocalDate获得年月日,LocalTime获得时分秒
转换对象的方法:
例子:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class DateDemo1 {
public static void main(String[] args) {
//LocalDate
LocalDate lo1=LocalDate.now();
//LocalTime
LocalTime lo2=LocalTime.now();
//LocalDateTime
LocalDateTime lo3=LocalDateTime.now();
System.out.println(lo1);
System.out.println(lo2);
System.out.println(lo3);
//of
LocalDate lo4=LocalDate.of(2000,10,1);
System.out.println(lo4);
//get,还有很多,就不一一展示了
lo4.getYear();
//注意,这里获得的是月的对象,直接打印会打印出对象的信息
//可以通过getValue方法来获取月份
//也可以直接通过getMonthValue方法来获取月的信息
System.out.println(lo4.getMonth().getValue());
System.out.println(lo4.getMonthValue());
System.out.println(lo4.getEra());
//is开头
System.out.println(lo4.isAfter(lo1));
System.out.println(lo4.isBefore(lo1));
//同理,with,minus,plus都是一样的使用方法
}
}
2.9.4 工具类(Duration,Period,ChronoUnit)
Period:计算年月日之间的时间间隔
import java.time.LocalDate;
import java.time.Period;
public class DateDemo1 {
public static void main(String[] args) {
LocalDate today=LocalDate.now();
System.out.println(today);
LocalDate oneDay=LocalDate.of(2004,8,9);
System.out.println(oneDay);
//第二个参数减去第一个参数
Period period= Period.between(oneDay,today);
//P18Y8M3D,这里period是一个对象,这里18Y就是相差18年,8M就是8个月,3D就是三天
System.out.println("相差的间隔时间对象"+period);
System.out.println(period.getYears());
System.out.println(period.getMonths());
System.out.println(period.getDays());
//toTotalMonths方法,可以获取的相差的总月数
System.out.println(period.toTotalMonths());
}
}
Duration:用于计算两个时间间隔
代码基本同理:
//第二个参数减去第一个参数
Duration duration=Duration.between(oneDay,today);
获取Duration对象,
获取相应的准确的时间间隔时,用to方法名
来获取
例如:
toDays()
就是获取天数的时间差
toHours()
就是获取小时的时间差
最小的单位可以达到纳秒
ChronoUnit:最常用的,因为他几乎包含了上面两个类的时间单位
public static void main(String[] args) {
LocalDateTime ldt1=LocalDateTime.now();
LocalDateTime ldt2=LocalDateTime.of(2001,10,10,10,10,10);
System.out.println("相差的年"+ ChronoUnit.YEARS.between(ldt2,ldt1));
System.out.println("相差的月"+ ChronoUnit.MONTHS.between(ldt2,ldt1));
System.out.println("相差的周"+ ChronoUnit.WEEKS.between(ldt2,ldt1));
System.out.println("相差的天"+ ChronoUnit.DAYS.between(ldt2,ldt1));
System.out.println("相差的时"+ ChronoUnit.HOURS.between(ldt2,ldt1));
System.out.println("相差的分"+ ChronoUnit.MINUTES.between(ldt2,ldt1));
System.out.println("相差的秒"+ ChronoUnit.SECONDS.between(ldt2,ldt1));
System.out.println("相差的毫秒"+ ChronoUnit.MILLIS.between(ldt2,ldt1));
System.out.println("相差的纳秒"+ ChronoUnit.MICROS.between(ldt2,ldt1));
System.out.println("相差的半天"+ ChronoUnit.HALF_DAYS.between(ldt2,ldt1));
System.out.println("相差的十年"+ ChronoUnit.DECADES.between(ldt2,ldt1));
}
2.10 包装类
包装类:基本数据类型对应的引用类型(对象)
内存的角度理解包装类:
即,用一个对象,把基本数据类型给包起来,就是包装类
所有基本数据类型的包装类
获取Integer对象(JDK5之前):
- 构造方法和静态方法获取Integer对象的区别
用静态方法获取Integer对象时,如果对象的数值在-128到127之间,就会使用已经创建好的对象,像字符串的串池那样,使用时直接调用,而不是重新创建
例子:
public class DateDemo1 {
public static void main(String[] args) {
//1.利用构造方法获取Integer的对象(JDK5以前的方式)
Integer i1 = new Integer(1);
Integer i2 = new Integer("2");
System.out.println(i1);//1
System.out.println(i2);//2
//2.利用静态方法获取Integer的对象(JDK5以前的方式)
Integer i3 = Integer.valueOf(123);
Integer i4 = Integer.valueOf("123");
Integer i5 = Integer.valueOf("123",8);
System.out.println(i3);//123
System.out.println(i4);//123
System.out.println(i5);//83
//3.这两种方式获取对象的区别(要求掌握)
Integer i6 = Integer.valueOf(127);
Integer i7 = Integer.valueOf("127");
System.out.println(i6 == i7);//true
Integer i8 = Integer.valueOf(128);
Integer i9 = Integer.valueOf(128);
System.out.println(i8 == i9);//false
//因为看到了new关键字,在Java中,每一次new都是创建了一个新的对象
//所以下面的两个对象都是new出来,地址值不一样。
Integer i10 = new Integer(27);
Integer i11 = new Integer("127");
System.out.println(i10 == i11); //false
Integer i12 = new Integer(128);
Integer i13 = new Integer(128);
System.out.println(i10 == i11); //false
}
}
jdk5之后的改进:
即在jdk5以后,int和Integer可以看作是同一个东西,因为在内部可以自动转化
小结:
lnteger成员方法:
都是静态方法,可以用类名.
调用
public class IntegerTest {
public static void main(String[] args) {
String s1=Integer.toBinaryString(100);
String s2=Integer.toOctalString(100);
String s3=Integer.toHexString(100);
int i=Integer.parseInt("100");
boolean b=String.parseBoolean("true");
sout(b);//true
//在类型转换时,参数只能是数字
//8中包装类中,除了Character其他都有相应的parseXXx方法,进行类型转换
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
System.out.println(i);
}
}
public static int parselent(String s) 将字符串类型的整数转化为int类型的整数,
这个以后用的最多
以后如果想要键盘录入的话,统一使用nextLine,然后用相应包装类的转换方法把类型转换,这里接收nextLine的参数尽量是String类型
即:
Scanner sc=new Scanner(System.in);
String line=sc.nexeLine();
2.11 综合练习
2.11.1 键盘录入
import java.util.ArrayList;
import java.util.Scanner;
public class one {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
ArrayList<Integer>ig=new ArrayList<>();
while (true){
System.out.println("请输入一个整数");
String numStr = sc.nextLine();
int i=Integer.parseInt(numStr);
if(i<0||i>100){
System.out.println("输入的数据有误");
continue;
}
//这里注意
//i是基本数据类型
//集合中只能存储引用数据类型
//所以在底层,这里的i已经自动装箱变成了Integer类型
ig.add(i);
int sum=getSum(ig);
if(sum>200){
System.out.println("计算结束");
break;
}
}
}
private static int getSum(ArrayList<Integer> ig) {
int sum=0;
for (int i = 0; i < ig.size(); i++) {
int a=ig.get(i);
sum=sum+a;
}
return sum;
}
}
2.11.2 算法水题一:将字符串转化整数
public class two {
public static void main(String[] args) {
String s="12345678";
if(!s.matches("[1-9]\\d{0,9}")){
System.out.println("数据有误");
}else{
int sum=0;
for (int i = 0; i < s.length(); i++) {
char a=s.charAt(i);
int b=a-'0';
sum=sum*10+b;
}
System.out.println(sum+1);
}
}
}
2.11.3 算法水题二:十进制转二进制
public class three {
public static void main(String[] args) {
System.out.println(getBinaryString(6));
// System.out.println(Integer.toBinaryString(100));
}
public static String getBinaryString(int a){
StringBuilder sb=new StringBuilder();
while (true){
if (a==0){
break;
}
System.out.println(a);
int b=a%2;
sb.insert(0,b);
a=a/2;
}
return sb.toString();
}
}
2.11.4 算法水题三:计算活了多少天
jdk7:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class four {
public static void main(String[] args) throws ParseException {
String s="2004年8月9日";
SimpleDateFormat sdf=new SimpleDateFormat("yyyy年MM月dd日");
Date d=sdf.parse(s);
long time=d.getTime();
// Date now=new Date();
// long time1=now.getTime();//这种方法不好
//可以直接获取当前时间的毫秒值
long time1=System.currentTimeMillis();
long time2=time1-time;
long day=time2/3600/1000/24;
System.out.println(day);
}
}
jdk8:
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class five {
public static void main(String[] args) {
LocalDateTime l1=LocalDateTime.now();
LocalDateTime l2=LocalDateTime.of(2004,8,9,0,0,0);
System.out.println(ChronoUnit.DAYS.between(l2,l1));
}
}
2.11.5 算法水题四:判断闰年还是平年
import java.time.LocalDate;
import java.util.Calendar;
public class six {
public static void main(String[] args) {
//jdk7
Calendar c=Calendar.getInstance();
c.set(2000,2,1);//月份的范围0-11
c.add(Calendar.DAY_OF_MONTH,-1);
int day1=c.get(Calendar.DAY_OF_MONTH);
System.out.println(day1);
//获取一年中的天数,判断天数是否为366天,若是,就是闰年,不是就不是闰年
//jdk8
//一个方法就直接搞定
//isLeapYear,判断是否为闰年,是就是true,否就是false
LocalDate l1=LocalDate.of(2000,2,1);
System.out.println(l1.isLeapYear());
}
}
2.12 Arrays
Arrays就是操作数组的工具类
里面的方法基本上都是用static修饰的,想要调用不需要创建对象,直接类名调用就行了
import java.util.Arrays;
import java.util.Comparator;
public class ArraysTest {
public static void main(String[] args) {
int []arr={1,2,3,4,5,3,1,7,8,4,8,9};
//toString
String a= Arrays.toString(arr);
System.out.println(a+1);
//binarySearch
//二分查找的前提:使用binarySearch数组的元素必须时有序的,二分查找
//如果查找的元素存在,返回真实索引,不存在,则返回 -插入点-1(负的插入点减1)(插入点也按照大小顺序)
//为什么要 -1呢?
//解释:如果此时,我现在要查找数字0,那么如果返回的值是-插入点,就会出现问题了。如果要查找数字0,此时0是不存在的,但是按照上面的规则-插入点,应该就是-0
//为了避免这样的情况, Java在这个基础上又减一
int b=Arrays.binarySearch(arr,7);
System.out.println(b);
//copyOf
//第一个参数是老数组,第二个是新数组的长度
//若新数组的长度小于老数组,部分拷贝,等于全部拷贝
//大于就全部拷贝的同时,补上数组默认初始化值
int []copyArr=Arrays.copyOf(arr,12);
System.out.println(Arrays.toString(copyArr));
//copyOfRange(指定范围)
//包头不包尾,包左不包右
int []arr3=Arrays.copyOfRange(arr,3,9);
System.out.println(Arrays.toString(arr3));
//fill,原来的数据全部被覆盖
Arrays.fill(arr,10);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println(" ");
//sort,默认情况下。给基本数据类型进行升序排列,底层使用的是快速排序
int []arr2={3,2,4,5,8,1,9,3};
Arrays.sort(arr2);
System.out.println(Arrays.toString(arr2));
//sort(数组,指定的规则),可以指定排序的规则
//只能给引用数据类型的数据进行排序
//如果是基本数据类型,需要变成对应的包装类
Integer []arr5={3,2,4,5,8,1,9,8,7,6};
//第二个参数是一个接口,需要传递这个接口的实现类对象,作为排序的规则
//,但是这个实现类只需要执行一次就行,所以没有必要单独写一个类,直接用匿名内部类就行了
//返回值:o1-o2:升序排列,o2-o1:降序排列
//底层原理;
//利用插入排序+二分查找的方式进行排序的。
//默认把0索引的数据当做是有序的序列,1索引到最后认为是无序的序列。
//遍历无序的序列得到里面的每一个元素,假设当前遍历得到的元素是A元素
//把A往有序序列中进行插入,在插入的时候,是利用二分查找确定A元素的插入点。
//拿着A元素,跟插入点的元素进行比较,比较的规则就是compare方法的方法体如果方法的返回值是负数,拿着A继续跟前面的数据进行比较
//如果方法的返回值是正数,拿着A继续跟后面的数据进行比较
// 如果方法的返回值是0,也拿着A跟后面的数据进行比较
// 直到能确定A的最终位置为止
//compare方法的形式参数:
//参数一o1:表示在无序序列中,遍历得到的每一个元素
//参数二 o2:有序序列中的元素
//返回值:
//负数:表示当前要插入的元素是小的,放在前面
//正数:表示当前要插入的元素是大的,放在后面
//0:表示当前要插入的元素跟现在的元素比是一样的们也会放在后面
Arrays.sort(arr5, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
System.out.println(Arrays.toString(arr5));
}
}
记住一句:o1-o2:升序排列,o2-o1:降序排列
2.13 Lambda表达式
Lambda表达式
用来简化内部类的书写:
//不用Lambda表达式时
Arrays.sort(arr5, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
//用Lambda表达式时
Arrays.sort(arr5, new (Integer o1, Integer o2)-> {
return o1-o2;
}
);
2.13.1 函数式编程
函数式编程思想,忽略面向对象的复杂语法,强调做什么,而不是谁去做,而我们要学习的Lambda表达式就是函数式思想的体现。
2.13.2 Lambda表达式的标准格式
public class Main {
public static void main(String[] args) {
//Lambda表达式的注意点:
//1.Lambda表达式可以用来简化匿名内部类的书写
//2.Lambda表达式只能简化函数式接口的匿名内部类的写法
// 3.函数式接口:
// 有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@FunctionalInterface注解
//1.利用匿名内部类的形式去调用下面的方法
//调用一个方法的时候,如果方法的形参是一个接口,那么我们要传递这个接口的实现类对象
//如果实现类对象只要用到一次,就可以用匿名内部类的形式进行书写
// method(new Swim() {
// @Override
// public void swimming() {
// System.out.println("正在游泳");
// }
// });
method(() -> {
System.out.println("正在游泳");
}
);
}
public static void method(Swim s) {
s.swimming();
}
}
interface Swim {
public abstract void swimming();
}
小结:
2.13.3 Lambda表达式的省略写法
省略核心:可推导,可省略(即可以推导出来的都可以省略)
Lambda表达式的省略规则:
- 参数类型可以省略不写
- 如果只有一个参数, 参数类型可以省略,同时()也可以省略
- 如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,但是需要这三个同时省略
//不用Lambda表达式时
Arrays.sort(arr5, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
//Lambda表达式的完整格式
Arrays.sort(arr5, new (Integer o1, Integer o2)-> {
return o1-o2;
}
);
//Lambda表达式的省略写法
Arrays.sort(arr5, new (o1,o2)-> o1-o2);//最省略的写法
小结:
练习:
import java.util.Arrays;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
String[] arr = {"a", "aaaa", "aaa", "aa"};
//如果以后要把数组中的数据按照指定的方式进行排列,就需要用到sort方法,而且要指定排序的规则
//1.匿名类的写法
/*Arrays.sort(arr, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//字符串的长度进行排序
return o1.length()-o2.length();
}
});*/
//2.lambda 的完整写法
/*Arrays.sort(arr, (String o1, String o2)->{
return o1.length()-o2.length();
}
);*/
//3.lambda 的省略写法
Arrays.sort(arr, (o1, o2) -> o1.length() - o2.length());
//Arrays的将数组以字符串形式打印
System.out.println(Arrays.toString(arr));
}
}
3. 常见算法
3.1 查找算法
基本查找(对数据顺序没有要求)
定义一个容器,像是集合,数组等,然后通过索引一个一个的查找
核心:从零索引开始,挨个往后查找
public class chatOne {
public static void main(String[] args) {
}
public static boolean basicSearch(int []arr,int number){
for (int i = 0; i < arr.length; i++) {
if(arr[i]==number){
System.out.println("元素已被找到");
return true;
}
}
return false;
}
}
练习:
import java.util.ArrayList;
public class chatOne {
public static void main(String[] args) {
int []arr={1,2,3,4,4,4,4,5};
int number=4;
ArrayList<Integer>a=basicSearch2(arr,number);
System.out.println(a);
}
//不需要考虑重复
public static int basicSearch1(int []arr,int number){
for (int i = 0; i < arr.length; i++) {
if(arr[i]==number){
System.out.println("元素已被找到");
return i;
}
}
return -1;
}
//考虑重复
public static ArrayList<Integer> basicSearch2(int []arr , int number){
ArrayList<Integer> a=new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
if(arr[i]==number){
a.add(i);
}
}
return a;
}
}
3.1.1 二分查找
前提条件:数据必须是有序的
- 如果数据是无序的先排序再查找是没有意义的,因为只能确定当前数字在数组中是否存在,不能确定数字实际的索引值
核心逻辑:每次排除一半的查找范围
核心点:
这里的mid=(min+max)/2
- 如果所查找的元素的值在mid的左边,min不变,max=mid-1
- 如果所查找的元素的值在mid的右边,max不变,min=mid+1
min,max,mid都是索引值
练习:
小结:
public class chatTwo {
public static void main(String[] args) {
int []arr={7,23,79,81,100,123,192};
int a=200;
System.out.println(getIndex(arr,a));
}
public static int getIndex(int[]arr,int number){
int min=0;
int max=arr.length-1;
int mid=(min+max)/2;
while (true){
if(min>max){
return -1;
}
if(arr[mid]>number){
max=mid-1;
mid=(min+max)/2;
}else if(arr[mid]<number){
min=mid+1;
mid=(min+max)/2;
}
else{
return mid;
}
}
}
}
小结:
3.1.2 插值查找
二分查找改进1(插值查找)
要求:数据分布比较均匀
用一个公式使mid更接近实际的数据
mid=(key-arr[min])/(arr[max]-arr[min])*(max-min)
这个公式的解释就是
- 用所求值减去开头除以总的长度得到所求值在数组中所占的比例,再用这个比值乘以索引的长度得到所求值索引的估值
- 就比如,在一个xy坐标系上,一条直线的长度是10,用3除以10得到十分之三,那么,十分之三就是3这个点在这台哦直线上的比值,即0到3的长度等于10乘以十分之三
- 最后再加上min的原因是使查找范围进行一个偏移,因为查找范围可能不是从0开始
3.1.3 斐波那契查找
同样要求数据一定要有顺序
mid公式:
小结:
3.1.4 分块查找
分块的原则:
- 前一块中的最大数据,小于后一块中的所有数据(块内无序,块间有序)
- 块的数量一般等于数字的个数开根号,例如:16个数字一般分四块左右
核心思路:先确定要查找的元素在哪个块内,然后再块内挨个查找
定义一个块类,将不同的块创建不同的块对象:
用一个数组来存储这些块的对象,这个数组也叫做索引表
public class BlockTest {
public static void main(String[] args) {
int []arr={16,5,9,12,21,18,
32,23,37,25,45,34,
50,48,61,52,73,66};
Block one=new Block(21,0,5);
Block two=new Block(45,6,11);
Block three=new Block(73,12,17);
Block[]arr1={one,two,three};
int number=32;
//调用方法,获取所求元素的索引
int index=getIndex(arr1,arr,number);
System.out.println(index);
}
private static int getIndex(Block[] arr1, int[] arr, int number) {
int index=findIndexBlock(arr1,number);
int strIndex=arr1[index].getStarIndex();
int endIndex=arr1[index].getEndIndex();
for (int i = strIndex; i <=endIndex ; i++) {
if(number==arr[i]){
return i;
}
}
return -1;
}
//定义方法,确定number在哪一块中
public static int findIndexBlock(Block[]arr1,int number){
for (int i = 0; i < arr1.length; i++) {
if(number<=arr1[i].getMax()){
return i;
}
}
return -1;
}
}
class Block{
private int max;
private int starIndex;
private int endIndex;
public Block() {
}
public Block(int max, int starIndex, int endIndex) {
this.max = max;
this.starIndex = starIndex;
this.endIndex = endIndex;
}
public int getMax() {
return max;
}
public void setMax(int max) {
this.max = max;
}
public int getStarIndex() {
return starIndex;
}
public void setStarIndex(int starIndex) {
this.starIndex = starIndex;
}
public int getEndIndex() {
return endIndex;
}
public void setEndIndex(int endIndex) {
this.endIndex = endIndex;
}
}
一、扩展的分块查找1(无规律的数据)
上图的数据不满足分块查找的规律,所以就需要扩展,在分块时,块与块之间不能有交集。
public class DateDemo1 {
public static void main(String[] args) {
int[] arr = {
16, 5, 9, 12, 21, 18,
32, 23, 37, 28, 45, 34,
50, 48, 61, 52, 73, 66
};
//创建三个块的对象
Block b1 = new Block(21, 0, 5);
Block b2 = new Block(45, 6, 11);
Block b3 = new Block(73, 7, 17);
//定义数组用来管理三给块的对象(索引表)
Block[] blockArr = {b1, b2, b3};
//定义一个变量用来记录要查找的元素
int number = 73;
//调用方法,传递索引表,数组,要查找的元素
int index = getIndex(blockArr, arr, number);
System.out.println("索引值:"+index);
}
//利用分块查找的原理,查询number的索引
private static int getIndex(Block[] blockArr, int[] arr, int number) {
//1.确定number是在那一块当中
int indexBlock = findIndexBlock(blockArr,number);
if(indexBlock == -1){
//表示number不在数组当中
return -1;
}
//2.获取这一块的起始索引和结束索引
int startIndex = blockArr[indexBlock].getStartIndex();
int endIndex = blockArr[indexBlock].getEndIndex();
//3.遍历
for (int i = startIndex; i < endIndex; i++) {
if(arr[i]==number){
return i;
}
}
return -1;
}
//定义一个方法,用来确定number在哪一块当中
public static int findIndexBlock(Block[] blockArr, int number) {//100
// Block b1 = new Block(21, 0, 5); ----0
// Block b2 = new Block(45, 6, 11); ----1
// Block b3 = new Block(73, 7, 17); ----2
//从0索引开始遍历blockArr,如果number小于max,那么就表示number是在这一块当中的
for (int i = 0; i < blockArr.length; i++) {
if(number <=blockArr[i].getMax()){
return i;
}
}
return -1;
}
}
class Block {
private int max;//最大值
private int startIndex;//起始索引
private int endIndex;//索引
public Block() {
}
public Block(int max, int startIndex, int endIndex) {
this.max = max;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
/**
* 获取
*
* @return max
*/
public int getMax() {
return max;
}
/**
* 设置
*
* @param max
*/
public void setMax(int max) {
this.max = max;
}
/**
* 获取
*
* @return startIndex
*/
public int getStartIndex() {
return startIndex;
}
/**
* 设置
*
* @param startIndex
*/
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
/**
* 获取
*
* @return endIndex
*/
public int getEndIndex() {
return endIndex;
}
/**
* 设置
*
* @param endIndex
*/
public void setEndIndex(int endIndex) {
this.endIndex = endIndex;
}
public String toString() {
return "Block{max = " + max + ", startIndex = " + startIndex + ", endIndex = " + endIndex + "}";
}
}
二、扩展的分块查找2(查找的过程中还需要添加额外的数据,哈希查找)
分块后把一个数存在数组里,其余在范围内的数挂
在后面。
小结:
3.2 排序算法
3.2.1 冒泡排序
相邻的数据两两比较,小的放在前面,大的放在后面
- 第一轮结束,最大的数据就会放在最前面,即数组的最右面
- 第二轮循环就在剩余的元素里找最大的值就行了
一直循环到数组排列好为止
核心思想:
- 相邻的数据两两比较,小的放在前面,大的放在后面
- 第一轮循环后,最大值就已被找到,第二轮可以少循环一次
- 如果数组里有n组数据,总共执行n-1轮代码就可以
public class Main {
public static void main(String[] args) {
int[] arr = {2, 4, 3, 1, 5};
//外循环,表示循环的次数
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
printArr(arr);
}
}
private static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
}
}
3.2.2 选择排序
从0索引开始,拿着每一个索引上的元素跟后面的元素依次比较,小的放前面,大的放后面,以此类推
第一轮循环后,最小的数据已被确定,放在0索引的位置,所以,第二轮循环时,从1所索引开始循环
public class Main {
public static void main(String[] args) {
int[] arr = {2, 4, 5, 3, 1};
//外循环,表示循环的次数
//i:表示这一轮中,我拿着哪个索引上的数据跟后面的数据进行比较并交换
for (int i = 0; i < arr.length - 1; i++) {
//内循环:每一轮我要干什么事情?
// 拿着i跟i后面的数据进行比较交换
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
printArr(arr);
}
}
private static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
3.2.3 插入排序
将0索引的元素到N索引的元素看作是有序的,把N+1索引的元素到最后一个当成是无序的。遍历无序的数据,将遍历到的元素插入到有序数列中适当的位置,如遇到相同数据,插在后面。
N的范围:0~最大索引
public class Main {
public static void main(String[] args) {
int []arr={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
//第一步,先获取无序部分的起始索引
int strIndex = 0;
for (int i = 0; i < arr.length; i++) {
if(arr[i]>arr[i+1]){
strIndex=i+1;
break;
}
}
//遍历无序部分,获得每一个无序部分的值
for (int i = strIndex; i < arr.length; i++) {
//先记录当前要插入数据的索引
int j=i;
while (j>0&&arr[j]<arr[j-1]){
int temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
j--;
}
printArr(arr);
}
}
private static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
3.2.4 快速排序
第一轮把0索引的数字作为基准数,确定基准数在数组中正确的位置。比基准数小的全部在左边,比基准数大的全部在右边
核心思想:先移动end,再移动start
第一轮代码实现:
public class Test {
public static void main(String[] args) {
int[] arr = {6, 1, 2, 7, 9, 3, 4, 5, 10, 8};
quickSort(arr, 0, arr.length - 1);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void quickSort(int[] arr, int i, int j) {
//定义两个变量记录要查找的范围
int start = i;
int end = j;
//记录基准数
int basicNumber = arr[i];
//利用循环找到要交换的数字
while (start != end) {//当两个变量不相等时才循环运行
//注意:一定要先移动end,再移动start
//利用end 从后往前面开始找,找比基准数小的数字
while (true) {
if (end <= start || arr[end] < basicNumber) {//如果后面end数 <= 前面start数时停下 或 end指向位置的数<基准数时,循环也要停止,这就表示end找到了这个数
break;
}
end--;
}
//利用start 从前往后面开始找,找比基准数大的数字
while (true) {
if (end <= start || arr[start] > basicNumber) {//如果后面end数 <= 前面start数时停下 或 end指向位置的数<基准数时,循环也要停止,这就表示end找到了这个数
break;
}
start++;
}
//把end和start指向的元素进行交换
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
//当start和lend指向了同一个元素的时候,那么上面的循环就会结束
// 表示已经找到了基准数在数组中应存入的位置
//基准数归位
//就是拿着这个范围中的第一个数字,跟start指向的元袁进行交换
int temp = arr[i];
arr[i] = arr[start];
arr[start] = temp;
}
}
再次确定右边的块
public class Test {
public static void main(String[] args) {
int[] arr = {6, 1, 2, 7, 9, 3, 4, 5, 10, 8};
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int i, int j) {
//定义两个变量记录要查找的范围
int start = i;
int end = j;
if(start>end){
//递归的出口
return;
}
//记录基准数
int basicNumber = arr[i];
//利用循环找到要交换的数字
while (start != end) {//当两个变量不相等时才循环运行
//注意:一定要先移动end,再移动start
//利用end 从后往前面开始找,找比基准数小的数字
while (true) {
if (end <= start || arr[end] < basicNumber) {//如果后面end数 <= 前面start数时停下 或 end指向位置的数<基准数时,循环也要停止,这就表示end找到了这个数
break;
}
end--;
}
//利用start 从前往后面开始找,找比基准数大的数字
while (true) {
if (end <= start || arr[start] > basicNumber) {//如果后面end数 <= 前面start数时停下 或 end指向位置的数<基准数时,循环也要停止,这就表示end找到了这个数
break;
}
start++;
}
//把end和start指向的元素进行交换
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
printArr(arr);
}
//当start和lend指向了同一个元素的时候,那么上面的循环就会结束
// 表示已经找到了基准数在数组中应存入的位置
//基准数归位
//就是拿着这个范围中的第一个数字,跟start指向的元袁进行交换
int temp = arr[i];
arr[i] = arr[start];
arr[start] = temp;
//确定6左边的范围,重复刚刚所做的事情
quickSort(arr,i,start-1);
//确定右边的范围
quickSort(arr,start+1,j);
}
private static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
小结:
3.3 递归算法
递归算法:
递归指的是方法中调用方法本身的现象 递归的注意点:递归一定要有出口(不再调用自己),否则就会出现内存溢出,即不能无限循环的调用方法
递归作用:
把一个复杂问题层层转化为一个与原问题相似的规模较小的问题来求解
书写递归的两个核心:
- 找出口:什么时候不再调用方法
- 找规则:如何把大问题转化为规模较小的问题
练习1:
//100+99+98+。。。+1
public class diGui {
public static void main(String[] args) {
System.out.println(get(100));
}
public static int get(int a){
if(a==1){
return 1;
}
return a+get(a-1);
}
}
//5050
练习2:
//5的阶乘
public class diGui {
public static void main(String[] args) {
System.out.println(get(5));
}
public static int get(int a){
if(a==1){
return 1;
}
return a*get(a-1);
}
}
//120
注意:方法内部再次调用方法时,参数必须要更加的靠近出口
方法的返回值会返回到方法的调用处,内存图解:
这里递归到出口时,出口处的n=1返回给jc(1),jc(1)*2返回给jc(2),同理,一直返回到jc(5)即函数总体的返回值
3.4 综合练习
String的compareTo
方法,可以比较ascll表值来进行字符串的判断
String a="a";
String b="b";
sout(a.compareTo(b));//-1,证明a的ascll值小于b的值
String c="abc";
String d="abd";
sout(c.compareTo(d));//-1,先比较ab,都相同,再将c和d进行比较
3.4.1 按要求进行排序
import java.util.Arrays;
import java.util.Comparator;
public class Test1 {
public static void main(String[] args) {
GirlFriend gf1 = new GirlFriend("a", 19, 1.65);
GirlFriend gf2 = new GirlFriend("b", 18, 1.70);
GirlFriend gf3 = new GirlFriend("c", 20, 1.72);
//2.定义数组储存上面的一堆信息
GirlFriend[] arr = {gf1, gf2, gf3};
//3.利用Arrays的sort方法排序
/* Arrays.sort(arr, new Comparator<GirlFriend>() {
@Override
public int compare(GirlFriend o1, GirlFriend o2) {
//按照年龄排序
double temple = o1.getAge() - o2.getAge();
//如果年龄相等,则按身高排序
temple = temple == 0 ? o1.getHeight() - o2.getHeight() : temple;
//如果身高还相等,则按名字循序排序
temple = temple == 0 ? o1.getName().compareTo(o2.getName()):temple;
if(temple>0){
return 1;
}else if(temple <0){
return -1;
}else{
return 0;
}
}
});*/
//lambda 表达式
Arrays.sort(arr,( o1, o2)->{
//按照年龄排序
double temple = o1.getAge() - o2.getAge();
//如果年龄相等,则按身高排序
temple = temple == 0 ? o1.getHeight() - o2.getHeight() : temple;
//如果身高还相等,则按名字循序排序
temple = temple == 0 ? o1.getName().compareTo(o2.getName()):temple;
if(temple>0){
return 1;
}else if(temple <0){
return -1;
}else{
return 0;
}
});
System.out.println(Arrays.toString(arr));
}
}
3.4.2 不死神兔(斐波那契数列)
数据的特点:
- 从第三个月开始,每月的数量等于前两个月之和
public class Test1 {
public static void main(String[] args) {
//求解1:
/*
//1.创建一个长度为12的数组
int[] arr = new int[12];
//2.手动给0索引和1索引的数据进行赋值
arr[0] = 1;
arr[1] = 1;
//3.利用循环给剩余的数据进行赋值
for (int i = 2; i < arr.length; i++) {
arr[i] = arr[i-1]+arr[i-2];
}
System.out.println(arr[11]);
*/
//求解2:递归的方式去完成
//1.递归的出口
//2.找到递归的规律
//Fn(12) = Fn(11) +Fn(10);'
//Fn( 11) = Fn(10) + Fn(9);
//Fn( 10) = Fn(9) +Fn(8);
//....
//Fn(3) = Fn(1) + Fn(2);
//Fn(2) =1;
//Fn(1) = 1;
System.out.println(getSum(12));
}
public static int getSum(int number) {
if (number == 1 | number == 2) {
return 1;
} else {
return getSum(number - 1) + getSum(number - 2);
}
}
}
3.4.3 猴子吃桃子
public class Test1 {
public static void main(String[] args) {
/*
day10: 1
day9: (day10 + 1)* 2 =4
day8: (day9 + 1)* 2= 10
每一天的桃子数量都是后一天数量加1,乘以2
1.出口
day== 10剩下1
2.规律
每一天的桃子数量都是后一天数量加1,乘以2
*/
System.out.println(getCount(1));//第一天有多少
}
public static int getCount(int day){
if(day<=0||day>=11){
System.out.println("输入的数据错误");
return -1;
}
//递归出口
if(day==10){
return 1;
}
//规律:每一天的桃子数量都是后一天数量加1,乘以2
return (getCount(day+1)+1)*2;
}
}
3.4.4 爬楼梯
- 假设有一个方法已经计算完了前面19阶的爬法,那么爬到20阶,只需要思考19阶到20阶的爬法,显然就只有1种。那么在加上前面假设fn(19)的爬法即可得知总爬法。
有的人会想显然想到2种再用fn(18)+2,但是是不对的
如果爬到19阶时,就已经包含fn(19)爬法了,为了不重复计算爬楼梯的方法
20层台阶的爬法=19层台阶的爬法+18层台阶的爬法
还是斐波那契数列
那么在17阶开始爬时是怎么样的呢?
如果爬1到18阶,那么又重复了fn(18)方法;如果爬2到19阶,那么又重复了fn(19)
代码实现:
小明一次能爬1阶楼梯,或者2阶楼梯,或者3阶楼梯,当有n阶楼梯的时候
小明有多少种方法爬楼梯?
public class Test1 {
public static void main(String[] args) {
System.out.println(getPa(20));
}
public static int getPa(int number){
if(number==0){//当最前面依次走3阶
return 1;
}
if(number==1){//当最前面依次走1阶
return 1;
}
if(number==2){//当最前面依次走2阶
return 2;
}
return getPa(number-1)+getPa(number-2)+getPa(number-3);
}
}
4. 集合的体系结构
ArrayList只是集合的一个基础而已
在java中有很多集合,这些集合大体可以分为两类
- 单列集合(Collection),添加数据的时候每次只能添加一个元素
- 双列集合(Map),添加数据的时候每次添加一对数据
单列集合:
上面Collection,List,Set是接口,其余的都是这三个接口的实现类
-
List系列的集合的特点:
添加的元素是有序,可重复,有索引的
有序:存储和取出的顺序是一样的
可重复:集合中存储的元素是可以重复的
有索引:可以通过索引获取集合中的每一个元素 -
Set系列的集合的特点:
添加的元素是无序,不重复,无索引的
有序:存储和取出的顺序有可能是不一样的
可重复:集合中存储的元素是不可以重复的
有索引:不可以通过索引获取集合中的每一个元素
4.1 Collection
Collection是一个接口,不能创建他的对象,只能创建他的实现类对象,实现类就像ArrayList
import java.util.ArrayList;
import java.util.Collection;
public class Test1 {
public static void main(String[] args) {
//为了学习Collection的方法,先这样创建对象
Collection<String> coll = new ArrayList<>();
//1.add
//如果向List系列中添加元素,表达式会永远返回true,
//即c.add("aa")的返回值为true
//但是,向Set系列添加元素的话,
//如果当前要添加的元素在Set系列的集合不存在,结果返回true,
//如果当前要添加的元素在Set系列的集合存在,结果就会返回false
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);
//2.清空
//coll.clear();
//remove,因为Collection中定义的是共性的方法,
//所以不能通过索引删除,只能通过对象删除
//返回值为布尔类型,如果要删除的元素不存在,就会删除失败,返回false
coll.remove("ccc");
System.out.println(coll);
//contains
//contains底层是通过equals方法来判断是否存在的
//所以,如果集合中存储的是自定义对象,也想通过contains方法来判断是否包含
// 那么在自定义对象的JavaBean类中,一定要重写equals方法
//如果不重写,就会使用Object中的equals方法,对地址值进行判断
//
boolean b=coll.contains("ccc");
System.out.println(b);
//isEmpty,集合为空返回true,不为空返回false
System.out.println(coll.isEmpty());
//size获取集合的长度
System.out.println(coll.size());
}
}
例子:
import java.util.ArrayList;
import java.util.Collection;
public class CollectionDemo {
public static void main(String[] args) {
//1.创建集合的对象
Collection<Student> coll = new ArrayList<Student>();
//2.创建三个学生对象
Student s1 = new Student("magua",23);
Student s2 = new Student("lisi",24);
Student s3 = new Student("zhangsan",25);
//3.把学生对象添加到集合当中
coll.add(s1);
coll.add(s2);
coll.add(s3);
//4.判断集合钟某一个学生对象是否包含
Student s4 = new Student("zhangsan",25);
//如果同姓名和同年龄,就认为是同一个学生
System.out.println(coll.contains(s4));//false
//因为contains方法在底层依赖equals方法判断对象是否一致的。
//如果存的是自定义对象,没有重写equals方法,那么默认使用object类中的equals方j法进行判雨断,而iobject类中equals方法,依着地址值进行判断。
//需求:如果同姓名和同年龄,就认为是同一个学生
//所以,需要在自定义的Javabean类中,重写equals方法抗可以了
}
}
在Student类中,重写equals方法,比较的就是属性值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
4.1.1 Collection的遍历方式
一、迭代器遍历
迭代器遍历时不依赖索引
迭代器在Java中的类是Iterator
,迭代器是集合专用的遍历方式
图示的情况就是指针指向的位置没有元素,所以,这里的hasNext就会返回false
import java.util.*;
public class Main {
public static void main(String[] args) {
//1.创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//2.获取迭代器对象
//迭代器就好比一个箭头,默认指向集合的0索引处
Iterator<String> it = coll.iterator();
//3.利用循环不断的去获取集合中的每一个元素
while(it.hasNext()){
//4.next方法的两件事情:获取元素并移动指针
String str = it.next();
System.out.println(str);
}
}
}
迭代器书写时需要注意的细节
- 当指针指空时,即上图所示,再执行next方法就会报错
NoSuchElementException
- 迭代器遍历完毕后,指针不会复位,就会一直保留在最后的位置,如果想要再次遍历一遍,就需要再创建一个新的迭代器对象
- 循环中只能用一个next方法,next与hasNext是配套使用的
- 迭代器遍历的时候(遍历的过程中,这点很重要),不能用集合的方法增加或删除,
如果实在要删除,可以用迭代器的remove进行删除
(迭代器名.remove()),没有增加的办法
小结:
二、增强for遍历
快速生成方式:集合的名字+for 回车
import java.util.*;
public class Main {
public static void main(String[] args) {
//1.创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//2.利用增强for进行遍历
//注意点:
//s其实就是一个第三方变量,在循环的过程中依次表示集合中的每一个数据
//快捷自动生成 coll.for
for(String s:coll){
s="qqq";
System.out.println(s);
}
System.out.println(coll);
}
}
增强for的细节:
三、Lambda表达式遍历
import java.util.*;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
//1.创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//2.先用匿名类的形式
//底层原理:
//其实也会自己遍历集合,依次得到每一个元素
//把得到的每一个元素,传递给下面的accept方法
// s依次表示集合中的每一个数据
/* coll.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
*/
//3.lambda表达式
coll.forEach(s -> System.out.println(s));
}
}
小结:
4.1.2 List集合
List是一个接口
一、List的特有方法
例子:
import java.util.*;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
//1.创建一个list集合
List<String> list = new ArrayList<>();
//2.添加元素
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println(list);
//void add(int index,E element) 在此集合中的指定位置插入指定的元素
//原来索引上的元素会依次往后移动
list.add(1,"FFFFF");
System.out.println(list);
//E remove(int index) 删除指定索引处的元素,返回被删除的元素
String remove = list.remove(0);
System.out.println(remove);
System.out.println(list);
String set = list.set(2,"eee");
System.out.println(set);
System.out.println(list);
String get = list.get(1);
System.out.println(get);
}
}
在调用方法时,如果方法出现重载现象,优先调用实参与形参类型一致的那个方法
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1);//这里删除的是2,也就是索引为1的值
//因为在调用方法时,如果方法出现重载现象,优先调用实参与形参类型一致的那个方法
//如果想使用对象删除的话,就手动装箱
Integer i=Integer.valueOf(1);
list.remove(i);
二、List集合的遍历方式(列表迭代器)
List有5种遍历方式,
迭代器,列表迭代器遍历(List独有),增强for,Lambda,普通for循环
这几种都跟上面的方式差不多
import java.util.*;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
//1.创建一个list集合
List<String> list = new ArrayList<>();
//2.添加元素
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println(list);
System.out.println("--------------");
//1.迭代器
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}
//2.增强for
//下面的变量s、其实就是一个第三方的变量而已。
// 在循环的过程中,依次表示集合中的每一个元素
System.out.println("--------------");
for (String s : list) {
System.out.println(s);
}
//3.Lambda表达式
//forEach方法的底层其实就是一个循环遍历,依次得到集合中的每一个元素
//并把每一个元素传递给下面的accept方法
//accept方法的形参s,依次表示集合中的每一个元素
System.out.println("--------------");
list.forEach(s-> System.out.println(s));
System.out.println("--------------");
//4.普通for循环
for (int i = 0; i < list.size(); i++) {
String s= list.get(i);
System.out.println(s);
}
}
}
主要是列表迭代器
列表迭代器的获取:
Listterator<数据类型>列表迭代器名=集合名.ListIterator()
元素的添加:
列表迭代器名.add(元素)
这里判断与获取元素跟迭代器是一样的
方法名 | 说明 |
---|---|
boolean hasNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
E next() | 获取当前位置的元素,并将迭代器对象移向下一个位置 |
列表迭代器的使用与迭代器没有什么区别,唯一不同的点就是,列表迭代器可以在遍历的过程中添加元素
只有指针在后面往前移动才能用
import java.util.*;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
//1.创建一个list集合
List<String> list = new ArrayList<>();
//2.添加元素
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println(list);
System.out.println("--------------");
//5.列表迭代器
//获取一个列表迭代器的对象,里面的指针默认也是指向0索引的
//其额外添加了一个方法:在遍历的过程中,可以添加元素
ListIterator<String> it =list.listIterator();
while(it.hasNext()){
String str = it.next();
if("bbb".equals(str)){
//qqq
it.add("qqq");
}
}
System.out.println(list);
}
}
小结:
4.1.2 - I ArrayList 底层原理
-
利用空参构造创建的集合,会在底层创建一个默认长度为0的数组,数组名叫
elementData
,同时会有一个成员变量size
记录元素的个数
-
当添加第一个数据时,底层会创建一个新的长度为10的数组。所以这里的elementData是在添加第一个元素的时候创建的
size在记录元素的个数的同时,也代表着元素下一次存入的位置
-
底层的数组存满时,会自动扩容1.5倍
即当数组存满后,底层会创建一个新的数组,数组的长度是原来数组的1.5倍,然后把原来数组的元素全部拷贝到新数组中
-
如果一次添加多个元素,1.5倍的新数组放不下,则新创建的数组以实际长度为准
elementData
是底层数组的名称
grow
就是扩容
ArrayList底层源码:
- add(E e)的源码:
- add(e,elementData,size)的源码:
- 空参grow的源码:
- 带参grow的源码:
第一次执行添加元素后,会一直递归到带参grow
的if-else的else方法体内,然后执行Math.max
的判断,返回一个长度为10的数组elementData
,这样之后,就算扩容完成了,然后接着执行add(e,elementData,size)
下面的语句,把想要添加的值赋给elementData[0]
,size=size+1=1
第一次添加数据的过程:
如果添加的数据超过了第一次数据的长度,即10,就会执行第二段
4.1.2 - II LinkedList 底层原理
LinkedList底层数据结构是双链表,查询慢,增删快,但是如果操作的是首尾元素,速度也是极快的
双向链表:
因为首尾元素操作起来比较快,所以LinkedList本身多了很多直接操作首尾元素的特有api
这些方法用的不多:
LinkedList的底层源码:
- 结点的源码,item是结点存储的数据,next是下一个结点的地址值,prev是上一个结点的地址值。
LinkedList的源码:
这三个成员变量分别是:
- size,记录结点的个数
- first,记录头结点
- last,记录尾结点
当使用LinkedList的空参构造时,上面三个成员变量就会创建出来,只不过是初始化值。
- add的源码,调用linkLast方法
- linkLast方法的源码
- add向集合中添加元素的过程
例如:
LinkedList <String> list=new LinkedList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
集合list第一次调用add方法添加元素“aaa”
时,会调用add方法中的linkLast
方法,同时把“aaa”
元素传递过去,此时在linkLast
方法中创建一个变量l用来记录LinkedList集合创建时的成员变量last(结点的地址)
的值
由于刚开始集合内没有元素(结点),所以此时l
的值为null
,然后linkLast方法会在集合list中创建一个新的结点newNode(结点1)
,在创建结点时把变量l ,“aaa”
和null传递过去,创建list集合中第一个结点,然后把newNode
的地址值赋值给成员变量last
,再做一个判断
如果此时l的值为null,就把newNode
的地址值赋值给成员变量first
反之把newNode的地址值
赋值给l.next
,然后成员变量size+1
等到集合list
第二次添加元素"bbb"
时,此时再调用linkLast
方法,还是把成员变量last
的值赋值给变量l
但是,此时成员变量last
的值不再是null
,而是上一次添加元素“aaa”
时所创建的结点(结点1)的地址值,同样,会再创建一个新结点(结点2),把这时候的变量l
(结点1的地址值) ,“bbb”
和null
传递过去
此时if做判断的时候,l
不为null
,所以把结点2的地址值赋值给了l.next
(结点1内的成员变量next
),然后size再+1
此时,结点1就是首结点,有数据和结点2的地址值,结点2就是尾结点,有结点1的地址值和数据。
就如下图所示。
同理,再添加新的结点也是这样的步骤
4.1.2 - III Vector(迭代器) 底层原理
例子:
ArrayList<String>list=new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//创建迭代器
Iterator<String>it=list.iterator();
while(it.hasNext()){//判断迭代器指向的位置是否为空
String str=it.next();//获取指向的元素,移动指针到下一个位置
sout(str);
}
-
创建iterator对象时,即使用iterator()创建迭代器对象时,实际上就是创建了一个内部类Itr的对象
-
内部类Itr
-
hasNext源码
会判断光标与迭代器内的长度(元素的个数)是否相等,相等就返回false,证明指针指到了空的位置,不等就返回true
-
next源码
checkForComodification()方法,判断并发修改异常:
如何避免并发修改异常:
4.1.3 set集合
Set也是一个接口
回顾一下Collection的常见方法:
练习:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetDemo1 {
public static void main(String[] args) {
//1.创建一个Set集合的对象
Set<String> s = new HashSet<>();
//2.添加元素
//如果当前元素是第一次添加,那么可以添加成功,返回true
// 如果当前元素是第二次添加,那么添加失败,返回false
s.add("麻瓜");
s.add("愣头青");
s.add("小老板");
s.add("zhangsan");
//3.打印set集合
//打印出来是无序的,存入和输出的循序可以不一样
//set集合没有通过索引获取值的
System.out.println(s);
System.out.println("-----------------");
//迭代器遍历
Iterator<String> it = s.iterator();
while(it.hasNext()){
//hasNext()方法一般用于返回布尔值
//如果有下一个输入值,果决方为true;反之,则为false
String str = it.next();
System.out.println(str);
}
System.out.println("-----------------");
//增强for遍历
for (String s1 : s) {
System.out.println(s1);
}
System.out.println("-----------------");
//lambda表达式
s.forEach(s2 -> System.out.println(s2));
}
}
小结:
4.1.3 - i HashSet
没有什么额外的方法,与上面集合的方法是一样的
哈希值(哈希表中非常重要的值)
哈希值:对象的整数表现形式
哈希表当中是有数组存在的,里面的数据不是从0索引开始往后存储的
而是根据int index=(数组长度-1)&哈希值
;来得出数据在数组中存储的位置
对象哈希值的特点:
例子:
一键重写hashCode,一直点击下一步,完成即可
Studnet类:
import java.util.Objects;
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}
}
测试类: public static void main(String[] args) {
//1.创建对象
Student s1 = new Student("magua",23);
Student s2 = new Student("zhangsan",24);
//2.如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
//如果已经重写hashcode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
System.out.println(s1.hashCode());//-1081623399
System.out.println(s2.hashCode());//-1461067291
//3.在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样。
// 哈希碰搤
System.out.println("abc".hashCode());//96354
System.out.println("acD".hashCode());//96354
}
一、jdk8之前HashSet的底层存储原理
- 在底层,创建一个默认长度为16,默认加载因子为0.75的数组,数组名为table,默认初始化值为null
- 根据元素的哈希值和数组的长度利用公式计算出应该存入的位置 int index=(数组长度-1)&哈希值;
- 根据计算的值把数据存入相应位置,判断存入位置的值是否为null,如果是null,则直接存入
- 如果存入位置的值不是null,就表示存入位置有元素,然后调用equals方法比较属性值。
- 如果属性值一样,就不会存入,不一样就存入数组,形成链表
jdk8之前,存入时是新元素存入数组,老的元素挂在新元素的下面
先用equals比较属性值
二、jdk8之后新元素直接挂在老元素的下面形成的链表
继续添加元素:
- 同样的,会用添加元素的哈希值跟数组的长度进行计算,算出应存入的位置
- 假设计算出来的位置还是跟上面的相同是4索引的位置,而这里的值不是null,就会调用
equals方法去比较
内部的属性值 - 如果4索引下面挂了一条链表,那么此时就会从链表的第一个元素开始调用equals方法,跟里面的元素挨个地进行比较,一样的话就会舍去不存,这样就可以
保证数据的唯一性
- 如果说跟这个链表的元素都不一样,才会添加新的元素,
jdk8之后就把它挂在新元素下面
- 加载因子的使用
当数组里存入了加载因子*数组长度
个元素后,数组就会扩容成原先的两倍
注意:
1.当链表长度大于8且数组长度大于64时
,此时HashSet的链表就会自动地转换成红黑树
2.如果集合中存储的是自定义对象,那么必须重写hashCode和equals方法
三、HashSet的三个问题
-
HashSet为什么存和取的顺序不一样?
因为向HashSet存储元素的时候,是通过数组和链表或红黑树来存储的,用哈希值将数据在数组中存到链表或红黑树时,存储的顺序和取出的顺序是不同的 -
HashSet为什么没有索引?
因为链表和红黑树,一个索引下挂着多种元素,所以取消了索引 -
HashSet是利用什么机制保证数据去重的?
用hashCode和equals来保证数据的唯一,如果HashSet储存的是自定义对象,那么一定要重写hashCode和equals方法
问题总结:
练习:
像java已经提供好的一些类(String Integer)就不用重写hashCode和equals方法,因为java源码已经重写了
//stu类
import java.util.Objects;
public class stu {
private int age;
private String name;
public stu() {
}
public stu(int age, String name) {
this.age = age;
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
stu stu = (stu) o;
return age == stu.age && Objects.equals(name, stu.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "stu{age = " + age + ", name = " + name + "}";
}
}
//测试类
import java.util.HashSet;
public class HashTest {
public static void main(String[] args) {
HashSet<stu>set=new HashSet<>();
set.add(new stu(18,"1111"));
set.add(new stu(19,"2222"));
set.add(new stu(19,"3333"));
set.add(new stu(19,"3333"));
//由于重写了equals和hashCode方法,所以set.add(new stu(19,"3333"));
//没有成功添加,因为重写方法之后比较的是属性值,而没有重写方法之前比较的
//是地址值
System.out.println(set);
for (stu stu : set) {
System.out.println(stu);
}
}
}
四、LinkedHashSet
即每个元素都会像双向链表的结点一样,除了存储着数据外,还存储着其他元素的地址值,存入的第一个元素就是头结点
在遍历的时候就会遵循添加进链表的数据进行遍历,存储和取出的元素顺序一致
代码实现:
同理上方student类的方法
import java.util.LinkedHashSet;
public class SetDemo1 {
public static void main(String[] args) {
//1.创建4个学生对象
Student s1= new Student("magua",23);
Student s2= new Student("zhangsan",21);
Student s3= new Student("lisi",26);
Student s4= new Student("magua",23);
//2.创建集合对象
LinkedHashSet<Student> lhs = new LinkedHashSet<>();
//3.添加元素
//LinkedHashSet可以保证数据加入的存储循序
System.out.println(lhs.add(s3));
System.out.println(lhs.add(s2));
System.out.println(lhs.add(s1));
System.out.println(lhs.add(s4));
//4.打印集合
System.out.println(lhs);
}
}
小结:
4.1.3 - ii TreeSet
练习:
import java.util.Iterator;
import java.util.TreeSet;
public class SetDemo1 {
public static void main(String[] args) {
//1.创建TreeSet对象
TreeSet<Integer> ts = new TreeSet<>();
//2.添加元素
ts.add(4);
ts.add(2);
ts.add(1);
ts.add(3);
//及时添加数据顺序不一样,再TreeSet里面还是会按照顺序排序
System.out.println(ts);
System.out.println("---------------------");
//遍历集合(3种)
//迭代器
Iterator<Integer> it = ts.iterator();
while(it.hasNext()){
int i = it.next();
System.out.println(i);
}
System.out.println("---------------------");
//增强for循环
for (Integer t : ts) {
System.out.println(t);
}
System.out.println("---------------------");
//lambda表达式
ts.forEach(s1-> System.out.println(s1));
}
}
一、TreeSet集合默认的规则
如果字符串的字符比较多,比较时跟字符串的长度没有关系,而是会先比较第一个字符,如果第一个字符小,就排在前面,如果第一个字符相同,再比较第二个字符,以此类推
练习:
Student类同上述一致
测试类:
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
Student s1= new Student("magua",23);
Student s2= new Student("zhangsan",21);
Student s3= new Student("lisi",26);
TreeSet<stu>ts=new TreeSet<>();
ts.add(s1);
ts.add(s2);
ts.add(s3);
System.out.println(ts);
}
}
代码执行后会报错,因为这里用的是自己定义的stu类,没有给TreeSet指定比较规则,所以就会报错
二、TreeSet的两种比较方式
使用的原则:默认使用第一种方式,如果第一种不能满足当前需求,那就使用第二种方式,如果方式1和方式2同时存在,以方式2为准
方式一:默认排序/自然排序
- 默认排序/自然排序:Javabean类实现Comparable接口指定比较规则
- 即让自己定义的JavaBean类实现Comparable接口并且重写里面的抽象方法,不需要重写hashCode和equals方法,因为hashCode是跟哈希表有关,而TreeSet的底层是红黑树
//stu类
package lainxi.two;
import java.util.Objects;
public class stu implements Comparable<stu>{
private int age;
private String name;
public stu() {
}
public stu(int age, String name) {
this.age = age;
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
stu stu = (stu) o;
return age == stu.age && Objects.equals(name, stu.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "stu{age = " + age + ", name = " + name + "}";
}
//这个方法就是实现Comparable接口后重写的抽象方法
@Override
public int compareTo(stu o) {
//this:表示当前要添加的元素
//o:表示当前已经在红黑树存在的元素
System.out.println("this:"+this);
System.out.println("o:"+o);
//在这个方法中,可以指定排序的规则
//例如,指定年龄的规则,按照年龄的升序进行排列
/*返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
* */
return this.getAge()-o.getAge();
}
}
//测试类
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
Student s1= new Student("magua",23);
Student s2= new Student("zhangsan",21);
Student s3= new Student("lisi",26);
TreeSet<stu>ts=new TreeSet<>();
ts.add(s1);
ts.add(s2);
ts.add(s3);
System.out.println(ts);
}
}
详解compareTo(stu o)方法:
//这个方法就是实现Comparable接口后重写的抽象方法
@Override
public int compareTo(stu o) {
//在这个方法中,可以指定排序的规则
//例如,指定年龄的规则,按照年龄的升序进行排列
return this.getAge()-o.getAge();
}
在这个方法中,this
表示当前要添加的元素,o
表示已经在红黑树中存在的元素,返回值如果是负数
,则会认为要添加的元素是小的,存红黑树的左边
返回值如果是正数
,则会认为要添加的元素是大的,存红黑树的右边
返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<stu>ts=new TreeSet<>();
ts.add(new stu(19,"aaa"));
ts.add(new stu(29,"aba"));
ts.add(new stu(20,"aac"));
ts.add(new stu(27,"bca"));
ts.add(new stu(19,"aaa"));
System.out.println(ts);
}
}
在上述代码中
第一次添加的是stu(19,"aaa")
,此时stu(19,"aaa")
作为红黑树的根节点,然后再添加stu(29,"aba")
,此时就要根据compareTo(stu o)
方法中指定的规则来比较红黑树结点中数据的大小,上面compareTo(stu o)
方法指定的规则是stu对象的age属性进行比较,即return this.getAge()-o.getAge();
,这里this
就表示当前要添加的元素,即stu(29,"aba"),this.getAge()就是29,然后减去o.getAge()
,这里的o就表示已经在红黑树中存在的元素,即stu(19,"aaa"),o.getAge()就是19,29-19>0
,所以表示新添加的数据比原本的数据大,所以添加到原来数据的右边,添加多个元素时根据红黑树的规则进行添加
方式二:比较器排序
比较器排序:创建TreeSet对象的时候,传递比较器Comparator指定规则
TreeSet< E> ts =new TreeSet<>(new Comparator< E>(){}
构造一个新的空 TreeSet,它根据指定比较器进行排序。
练习1:
import java.util.Comparator;
import java.util.TreeSet;
public class SetDemo1 {
public static void main(String[] args) {
//1.创建集合
//o1:表示当前要要添加的元素
//o2:表示已经在红黑树存在的元素
//返回值规则跟之前是一样的
//返回值规则与方法1一样
//返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
//返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
//返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
TreeSet<String> ts = new TreeSet<>(
(o1, o2) -> {
//按照长度排序
int i = o1.length() - o2.length();
//如果一样长(i=0)则按照首字母排序,用o1再次调用compareTo方法,把o2传递过去
//如果不是一样长,就以真正的长度为准
//由三目运算符计算
i = i == 0 ? o1.compareTo(o2) : i;
//compareTo返回值是整型,比较ASCII码,返回他们之间的长度差值
return i;
}
);
//2.添加元素
ts.add("c");
ts.add("ab");
ts.add("df");
ts.add("def");
//3.打印集合
System.out.println(ts);
}
}
练习2:
Studnet2类:
public class Student2 implements Comparable<Student2> {
//将类声明为实现给定的接口,关键字 implements
//Comparable 接口是一个泛型接口
//然后重写方法就可以了
private String name;
private int age;
private int chinese;
private int math;
private int english;
public Student2() {
}
public Student2(String name, int age, int chinese, int math, int english) {
this.name = name;
this.age = age;
this.chinese = chinese;
this.math = math;
this.english = english;
}
/**
* 获取
*
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
*
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
*
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
*
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
*
* @return chinese
*/
public int getChinese() {
return chinese;
}
/**
* 设置
*
* @param chinese
*/
public void setChinese(int chinese) {
this.chinese = chinese;
}
/**
* 获取
*
* @return math
*/
public int getMath() {
return math;
}
/**
* 设置
*
* @param math
*/
public void setMath(int math) {
this.math = math;
}
/**
* 获取
*
* @return english
*/
public int getEnglish() {
return english;
}
/**
* 设置
*
* @param english
*/
public void setEnglish(int english) {
this.english = english;
}
public String toString() {
return "Student2{name = " + name + ", age = " + age + ", chinese = " + chinese + ", math = " + math + ", english = " + english + "}";
}
@Override
public int compareTo(Student2 o) {
int sum1 = this.getChinese() + this.getMath() + this.getEnglish();
int sum2 = o.getChinese() + o.getMath() + o.getEnglish();
//比较总分
int i = sum1 - sum2;
//如果总分一样,则按照语文成绩排序
i = i == 0 ? this.getChinese() - o.getChinese() : i;
//比数学
i = i == 0 ? this.getMath() - o.getMath() : i;
//比年龄
i = i == 0 ? this.getAge() - o.getAge() : i;
//这里的comparaTo是字符串的方法,比姓名
i = i == 0 ? this.getName().compareTo(o.getName()) : i;
return i;
}
}
测试类:
import java.util.TreeSet;
public class SetDemo2 {
public static void main(String[] args) {
/*
采用方式:
第一种:默认排序/自然排序
第二种:比较器排序
默认情况下,用第一种排序方式,如果第一种不能满足当前的需求,采取第二种方式。
*/
Student2 s1 = new Student2("magua",23,90,99,50);
Student2 s2 = new Student2("zhangsan",24,90,98,50);
Student2 s3 = new Student2("lisi",25,95,100,30);
Student2 s4 = new Student2("wangwu",26,60,99,70);
Student2 s5 = new Student2("zhaoliu",26,70,80,70);
//2.创建集合
//默认ArrayList
//如果想要数据唯一,就用Set集合的H爱上Set
//如果既要唯一又要排序,就用TreeSet
TreeSet<Student2> ts = new TreeSet<>();
//3.添加元素进到集合中
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
for (Student2 t : ts) {
System.out.println(t);
}
}
}
小结:
使用的原则:
默认使用第一种方式,如果第一种不能满足当前需求,那就使用第二种方式,如果方式1和方式2同时存在,以方式2为准
4.2 泛型深入
泛型:jdk5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
泛型格式:<数据类型>
注意:泛型中只能写引用数据类型(若想写其他类型只能写对应的包装类)
没有泛型时,集合存储数据的方式:
import java.util.ArrayList;
import java.util.Iterator;
public class three {
public static void main(String[] args) {
//没有泛型时,集合存储数据的方式
//先创建一个集合,没有泛型,可以加任意的数据类型
ArrayList list=new ArrayList();
list.add("111");
list.add(222);
list.add(new Student(11,"11"));
//遍历集合
Iterator it = list.iterator();
while(it.hasNext()){
Object obj=it.next();
//这里虽然可以访问,但是无法使用集合元素的特有功能,
//就像多态,不能调用子类的特有功能
System.out.println(obj);
}
//即如果不给集合限定类型,集合中所有的元素的默认类型都会是Object类型
//Object类型在获取出来的时候是不能使用子类的特殊行为的,
//所以java推出了泛型,数据统一
}
}
4.2.1 泛型带来的好处
- 统一数据类型
- 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来
拓展:
- java中的泛型是
伪泛型
,就是假的类型限定,只在编译时期是有效的 - 即在编写java文件时,是真正存在泛型的。当java文件编译成 class 字节码文件时,泛型就会消失。这个过程叫做
泛型的擦除
- 也就是在编译的时候,会坚持添加的数据是不是字符串,如果不是直接报错
泛型只需记住一句话,即泛型的出现是为了统一数据类型
4.2.2 泛型的细节
泛型可以在很多地方进行定义:
4.2.3 泛型类
泛型类的练习:
新创建的泛型类:
import java.util.Arrays;
/*
当我在编写一个类的时候,如果不确定类型,那么这个类就可以定义为泛型类
*/
public class MyArrayList<E> {
Object[] obj = new Object[10];
int size;
/*
E:表示是不确定的类型。该类型在类名后面已经定义过了
e:形参的名字、变量名
*/
public boolean add(E e){
obj[size] = e;//把这形参添加到数组当中
size++;//size开始是0,每添加1次,size就++
return true;//本次添加成功
}
//在往外获取的时候做一个类型转换
public E get(int index){
//拿出来的是Object类型的,所以要在前面进行一个强转
return (E)obj[index];
}
//写toString 相当与打印对象的时候出现的不是地址值,而是属性值
@Override
public String toString() {
return Arrays.toString(obj);//把数组的元素都拼接成一个字符串,再进行返回
}
}
Test测试:
public class GenericsDemo2 {
public static void main(String[] args) {
MyArrayList<String> list = new MyArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println(list);
MyArrayList<Integer> list2 = new MyArrayList<>();
list2.add(123);
list2.add(456);
list2.add(789);
int i = list2.get(0);
System.out.println(i);
System.out.println(list2);
}
}
4.2.4 泛型方法
练习:
外部泛型方法:
import java.util.ArrayList;
public class ListUtil {
private ListUtil(){};
//类中定义一个静态方法addA11,用来添加多个集合的元素
/*
参数一:集合
参数二~最后:要添加的元素
*/
//把参数e1,e2,e3,e4 添加到list里面去的泛型方法
public static<E> void addAll(ArrayList<E> list,E e1,E e2,E e3,E e4){
list.add(e1);
list.add(e2);
list.add(e3);
list.add(e4);
}
//有很多个数据要求拿出来
public static<E> void addAll2(ArrayList<E>list , E...e){
for (E element : e) {
list.add(element);
}
}
public void show(){
System.out.println("magua");
}
}
测试类:
import java.util.ArrayList;
public class GenericsDemo3 {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ListUtil.addAll(list1,"aaa","bbb","ccc","ddd");
System.out.println(list1);
ArrayList<Integer> list2 = new ArrayList<>();
ListUtil.addAll2(list2,1,2,3,4,4,6,6,6,6,6,6,6,6,6,6,6,6);
System.out.println(list2);
}
}
4.2.5 泛型接口
- 1.实现类给出确定类型
//实现类有具体类型,后续使用时类的方法也要具体的数据类型
public class MyArrayList2 implements List<String> {}
//创建之后对上方的,alt + 回车 实现很多的关于确定类型的方法
由于MyArrayList2 后面没有 <>
,在调用的时候就不用再写泛型了
例子:
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class MyArrayList2 implements List<String> {
//一键生成的众多重写方法......
}
测试类:
//添加
public static void main(String[] args) {
MyArrayList2 list = new MyArrayList2();
list.add("12451513251245");
}
- 2.实现类延续泛型,创建对象时再确定具体数据类型
//实现类延续泛型,创建对象时再确定具体数据类型
public class MyArrayList3<E> implements List<E> {}
MyArrayList3<String>list=new MyArrayList3<>();
由于这次MyArrayList2 后面有 <E>
,在调用的时候就需要强调使用明确的类型
例子:
public class MyArrayList2 <E> implements List<E> {
//一键生成的众多重写方法......
}
测试类:
//添加
public static void main(String[] args) {
MyArrayList2<Integer> list = new MyArrayList2();
list.add(1423154);
}
4.2.6 泛型的通配符 ?
泛型本身不具备继承性,但是数据具备继承性
例子:
import java.util.ArrayList;
public class Test3 {
public static void main(String[] args) {
ArrayList<Fu>list1=new ArrayList<>();
ArrayList<Zi>list2=new ArrayList<>();
ArrayList<Sun>list3=new ArrayList<>();
//这里可以看出,泛型不具有继承性,从而产生报错
method(list1);
// method(list2); 报错
// method(list3); 报错
//但是数据是有继承性的,可以将子类的对象传到父类泛型的集合中去
list1.add(new Fu());
list1.add(new Zi());
list1.add(new Sun());
}
public static void method(ArrayList<Fu> f){
}
}
class Fu{}
class Zi extends Fu{}
class Sun extends Fu{}
此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。
//可以用泛型方法来搞定上面的需求
import java.util.ArrayList;
public class Test3 {
public static void main(String[] args) {
ArrayList<Fu>list1=new ArrayList<>();
ArrayList<Zi>list2=new ArrayList<>();
ArrayList<Sun>list3=new ArrayList<>();
ArrayList<Student>list4=new ArrayList<>();
method1(list1);
method1(list2);
method1(list3);
method1(list4);
method2(list1);
method2(list2);
method2(list3);
method2(list4);
}
//但是,使用泛型方法是有弊端的
//弊端:
// 泛型可以接受任意的数据,
// 但是此时却可以解释任意的数据类(比如Ye Fu Zi Student等等)
public static void <E>method1(ArrayList<E> e){
}
// 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi
//此时就可以使用泛型的通配符:
// ? 也表示不确定的类型
// 但 ? 可以进行类型的限定
// ? extends E:表示可以传递E或E的所有子类类型
// ? super E:表示可以传递E或E的所有父类类型
public static void method2(ArrayList<? extends FU> list){
}
}
class Fu{}
class Zi extends Fu{}
class Sun extends Fu{}
class Student {}
泛型的通配符:
?
表示不确定的类型,可以进行类型的限定? extends E:
表示可以传递E
或E
的所有子类类型? super E:
表示可以传递E
或E
的所有父类类型
注意,这里的
E
是一个明确的类型
通配符使用格式:
public static void getOne(ArrayList<? extents Fu> list){}
上述代码就表示这个方法的参数可以传递Fu或Fu的所有子类类型
应用场景:
- 如果在定义类,方法,接口的时候,如果类型不确定,就可以定义泛型类,泛型方法,泛型接口
- 如果类型不确定,但是知道只能传递某个继承体系中的,就可以用泛型的通配符
泛型通配符的关键点:
可以限定类型的范围
练习:
//动物类
public abstract class Animal {
private String name;
private int age;
public Animal() {
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "Animal{name = " + name + ", age = " + age + "}";
}
//创建方法同类方法体,所有动物都吃东西,但每个动物吃的东西不一样
//这时就定义一个抽象方法就行了 abstract
//创建抽象方法的时候报错了,这是alt回车,把Animal改成抽象类就行
public abstract void eat();
}
//Cat类
//1.继承抽象类,重写里面所有的抽象方法
//2.本身Cat也是一个抽象的,让cat的子类再重写重写方法
//此时明显1不行,因为不同猫的eat方法体不一样,所以本身也必须是抽象的才行,这样不同的猫类,才能获取猫科的抽象方法
//所以重写只能在每个不同的猫上
public abstract class Cat extends Animal{ }
//Dog类
public abstract class Dog extends Animal{
}
//LiHuaCat类
public class LiHuaCat extends Cat{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的狸花猫,正在吃鱼\n");
}
}
//PersianCat类
public class PersianCat extends Cat{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的波斯猫,正在吃小饼干\n");
}
}
//HaShiQi类
public class HaShiQi extends Dog{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的哈士奇,正在吃骨头,边吃边拆家\n");
}
}
//TaiDiDog类
public class TaiDiDog extends Dog{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的泰迪,正在吃骨头,边吃边踣\n");
}
}
//测试类
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
//创建相关集合
ArrayList<LiHuaCat> list1 = new ArrayList<>();
ArrayList<PersianCat> list2 = new ArrayList<>();
ArrayList<HaShiQi> list3 = new ArrayList<>();
ArrayList<TaiDiDog> list4 = new ArrayList<>();
//创建对象,把数据添加到对象当中,再把对象赋给集合中,然后把集合传到方法里,遍历eat方法
LiHuaCat d1 = new LiHuaCat();
d1.setName("magua");
d1.setAge(1);
list1.add(d1);
keepPet1(list1);
PersianCat d2 = new PersianCat();
d2.setName("zhangsan");
d2.setAge(2);
list2.add(d2);
keepPet1(list2);
System.out.println("---------------------");
HaShiQi d3 = new HaShiQi();
d3.setName("liso");
d3.setAge(3);
list3.add(d3);
keepPet2(list3);
TaiDiDog d4 = new TaiDiDog();
d4.setName("wangwu");
d4.setAge(4);
list4.add(d4);
keepPet2(list4);
System.out.println("---------------------");
keepPet3(list1);
keepPet3(list2);
keepPet3(list3);
keepPet3(list4);
}
//1.要求1:该方法能养所有品种的猫,但是不能养狗
//ArrayList<E>直接使用是不行的,因为这样前面要先static<E>定义
//但这样写就表示了所有的类型(不仅是猫,还要狗)都可以传递过去
//那么既然是猫,就用通配符猫科以下的就行了
public static void keepPet1(ArrayList<? extends Cat> list){
for (Cat cat : list) {
cat.eat();
}
}
//2.要求:该方法能养所有品种的狗,但是不能养猫
public static void keepPet2(ArrayList<? extends Dog> list2){
for (Dog dog : list2) {
dog.eat();
}
}
//3.要求:该方法能养所有的动物,但是不能传递其他类型
public static void keepPet3(ArrayList<? extends Animal> list3){
for (Animal animal : list3) {
animal.eat();
}
}
}
小结:
4.3 集合的使用场景
5. 数据结构
数据结构就是计算机存储,
这里是引用
组织数据的方式
不同的场景下选择不同的数据结构
数据结构的概述:
- 数据结构就是计算机存储,组织数据的方式 是指数据相互之间是以什么方式排列在一起的
- 数据结构是为了更加方便的管理和使用数据,需要结合具体的业务场景来选择
- 一般情况下,好的数据结构可以带来更高的运行或储存效率
抓住三点:
5.1 栈
栈的特点:后进先出,先进后出
一端开口(栈顶),一端封闭(栈底)
数据出栈的过程称为
出/弹栈
最先进去的元素就是栈底元素,最后进去的元素就是栈顶元素
就像手枪的弹匣,最先装入的子弹最后打出
java内部内存的栈内存也用的这个原理
5.2 队列
队列的特点:先进先出,后进后出
两端都开口,一端称为后端,一端称为前端
这个过程就像是排队买票:
小结队列与栈的区别:
5.3 数组
数组特点
- 查询速度快:通过地址值(找到整个数组)和索引(找到单独的元素)定位,查询任意数据耗时相同(数据在内存中是连续存储的)
- 删除效率低:要将原始数据删除,同时后面每个数据前移
- 添加效率极低:添加位置后的每个数据后移,再添加元素
与之相对立的数据结构就是链表
5.4 链表
链表特点:
- 查询速度慢:链表在查询的时候就会比较慢,无论查询哪个数据都要从头开始找 链表的增删相对快(相对数组)
- 链表的每一个元素称之为结点,每一个结点都是独立的对象,结点里会存储具体的数据和下一个结点的地址值(单向链表)
链表的添加:
在数据AC之间添加一个数据B
①数据B对应的下一个数据地址指向数据C
②数据A对应的下一个数据地址指向数据B
链表的删除:
删除数据BD之间的数据C
①数据B对应的下一个数据地址指向数据D
②数据C删除
链表的扩展(双向链表):
即在原有的基础上再结点内添加了上一个结点的地址值
可以双向查找,提高了一点查询效率
小结:
5.5 树
树里的每一个元素都被叫做节点(Node)
树的一些名词:
- 每一个节点都是独立的对象,里面存放着:父节点的地址值,数据(值),左子节点的地址值,右子节点的地址值
- 当没有父节点或子节点时,就会记为null,每一个节点的子节点的数量就是度
- 22,18,26的度都是2
- 所以,在二叉树中,任意节点的度都要小于等于2
- 书的高度就是节点的,节数(层数),有几节(层)就是多少高度
在树中,最上面的节点就是根节点
蓝色虚线的部分就是根节点的左子树
同理,绿色虚线的部分就是根节点的右子树
子节点也有左右子树
5.5.1 二叉树
二叉树中,任意节点的度<=2
5.5.1 - i 二叉查找树
二叉查找树的添加节点:
- 在二叉查找树添加节点时遵循一个原则
小的存左边,大的存右边,一样的不存
二叉查找树的查找节点:
- 也是遵循
小的查左边,大的查右边,一样的就是结果
即会先将数据与根节点比较,若比根节点小,就会查找根节点的左边,反之查右边
举例找5:
1.5<7,所以找7的左子树
2. 5>4,所以找4的右子树
3. 最后找到
一、二叉查找树的弊端
若数据只是单向增大或减小,就会变得跟链表一样,要一个一个去找非常麻烦
所以,要引出平衡二叉树来提高查询效率
二、平衡二叉树
平衡二叉树在二叉查找树的基础上又多了一个规则:
任意节点
左右子树高度差不超过1
不属于平衡二叉树的例子:
属于平衡二叉树的例子:
这里的任意一定要注意
树从二叉树演变到二叉查找树,再演变到平衡二叉树
树的演变:
三、平衡二叉树的旋转机制
- 平衡二叉树通过旋转机制来保持二叉树的平衡
平衡二叉树有两种旋转机制:
- 左旋,右旋
- 只有当添加一个节点后,该树不是一个平衡二叉树的时候才会触发旋转机制。
- 旋转之后会重新保证树的平衡
- 但是如果添加一个节点后,该树还是一个平衡二叉树,那么就不会触发旋转机制
左旋:
旋转的时候先确定支点
确定支点:从添加的节点开始,不断的往父节点找不平衡的节点,把遇到的第一个不平衡的点当作支点,再通过支点进行旋转
非根节点旋转的步骤:
- 以遇到的第一个不平衡的点作为支点(确定支点)
- 把支点左旋降级,变成左子节点
- 晋升原来的右子节点
由图可知,因为到10这个节点时,右子树高度为0,左子树高度为2,2-0>1,所以10为第一个遇到的不平衡的节点,就把10作为支点,进行旋转:
旋转之后就是这样,10节点左旋降级,11节点晋升:
根节点旋转的步骤:
- 以不平衡的点(根节点)作为支点(确定支点)
- 将根节点往左拉
- 原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点
例子:
1.这里的7右子树高度为1,左子树高度为3,3-1>1,所以即根节点是一个支点
2.然后先把10的左子节点9给忽略,按照非根节点旋转的步骤来
3.然后再把10的左子节点给7(之前的根节点),作为7的右子节点
应用实例图:
1.
2.
右旋:
确定支点:从添加的节点开始,不断的往父节点找不平衡的节点,把遇到的第一个不平衡的点当作支点,再通过支点进行旋转
非根节点旋转的步骤:
- 以遇到的第一个不平衡的点作为支点(确定支点)
- 把支点右旋降级,变成右子节点
- 晋升原来的左子节点
例子1:
根节点旋转的步骤:
- 以不平衡的点(根节点)作为支点(确定支点)
- 将根节点往右拉
- 原先的左子节点变成新的父节点,并把多余的右子节点出让,给已经降级的根节点当左子节点
例子2:
与上面左旋是反过来的
1.由于7这个节点的左子树高度为3,右子树高度为1,3-1>1,所以以此作为支点,右旋
2.
应用实例图:
2.
3,
四、平衡二叉树需要旋转的四种情况
左左:
左左:一次右旋
当根节点左子树的左子树有节点插入,导致二叉树不平衡的情况就是左左
如果是左左导致的不平衡的话,一次右旋就可以搞定了
- 7是根节点,7的左子树就是蓝色虚线部分,7的左子树的左子树就是红色部分,即7的左子树的左子树就是4
下图的两种情况就是在根节点的左子树的左子树上插入了一个节点导致不平衡
然后做一次右旋就可以了
左右:
左右:先局部左旋,再整体右旋
当根节点的左子树的右子树有节点插入,导致二叉树不平衡的情况就是左右
这种情况一次旋转就搞不定了,要先局部左旋,再整体右旋
需要先在局部进行一次左旋,使二叉树变成左左的情况,紫色就是局部左旋的部分
完成后变成左左,再进行右旋
右右:
右右:一次左旋
当根节点右子树的右子树有节点插入,导致二叉树不平衡的情况就是右右
跟左左相反,一次左旋就可以了
右左:
右左:先局部右旋旋,再整体左旋
当根节点的右子树的左子树有节点插入,导致二叉树不平衡的情况就是右左
跟左右相反,这种情况一次旋转就搞不定了,要先局部右旋旋,再整体左旋
小结:
一些平衡二叉树的问题:
1(第一问)平衡二叉树是一种特殊的二叉查找树,所以平衡二叉树节点的添加也遵循二叉查找树的方式,小的存左边,大的存右边,一样的不存
2(第二问)查找节点从根节点开始找,将节点值与根节点进行比较,若比根节点小,就再与根节点的左子节点比较,同理,若比根节点大,就与根节点的右子节点比较,如果不相等,就重复以上步骤
3.(第三问)平衡二叉树才需要旋转,还有红黑树。旋转的目的:当添加一个节点的时候,导致一个树不平衡了,那么就需要用到旋转让这个树保持重新平衡
4.(第四问) 当成功添加节点时,破坏了树的平衡,就需要进行旋转;没有则不需要
5.5.1 - ii 二叉树的遍历方式
一、前序遍历
即中(根节点)左(子树)右(子树)的方式遍历
根节点,左子树,右子树
那么遍历从20开始,然后18,16,19,23,22,24
这就是前序遍历,
二、中序遍历
遍历从16开始,然后18,19,20,22,23,24
从小到大的方式获取
三、后序遍历
从16开始,然后19,18,22,24,23,20
四、层序遍历
层序遍历,从上到下
20,18,23,16,19,22,24
小结:
即前序遍历获取当前节点是从最前面获取,
中序就是中间的时候获取,后序就是最后获取
5.5.1 - iii 红黑树
红黑树与平衡二叉树的区别:
在平衡二叉树里:
优点:高度平衡,查找效率比较高
缺点:每次当左右子树高度差超过1时,都要通过旋转保持平衡,太繁琐
一、红黑规则
简单路径:指从某一节点开始,一直往子节点前进,不能后退的路径
二叉树节点的组成:
而红黑树节点的组成:
二、红黑树添加节点的规则
- 默认添加颜色:添加的节点默认是红色的(效率高)
由于规则5,导致添加红色节点的效率高
不满足红黑规则的情况:
其违背了规则五,那么把18这个节点调为红色的就行,然后再把23这个节点加入根节点的右子树,也调为红色
所以如果说添加三个节点,要调整两次
三、添加节点的处理规则
这个表应该会很常用(在红黑树中)
这个后面的具体步骤就看上面的图
红黑树的增删改查的性能都很好