String 字符串
String是一个类,属于数据类型中的引用类型
Java的一切使用""引起了的内容 都是这个类的实例。称为字符串对象
字符串在定义后。值不可改变,是一个常量。实际是一个字符数组
![](https://i-blog.csdnimg.cn/blog_migrate/b4b514b1195116a43d9b82beba03f9b3.png)
String类使用时注意
由此可以,如果要频繁改动String类型变量的值。会创建很多个字符串对象,效率很低
所以在频繁改动字符串时,不要使用String类
如果要频繁改的字符串时,使用 StringBuilder 或 StringBuffer类
如何创建字符串对象
使用""赋值创建
Stringstr="abc";
2.通过构造方法创建
常用构造方法 | 说明 |
String() | 创建一个空白字符串对象,即"" |
String(String str) | 创建一个指定字符串对象 |
String(char[] list) | 创建一个指定字符串数组的字符串对象 |
String(byte[] bytes,String charsetName) | 按指定的编码格式创建一个指定字节数组的字符串对象 |
字符串String类中的常用方法
方法名 | 返回值 | 作用 |
length() 长度 | int | 得到字符串长度 |
toLowerCase() | String | 转换为小写 |
toUpperCase() | String | 转换为大写 |
trim() | String | 去除字符串的首尾全部空格 |
isEmpty() | boolean | 判断字符串长度是否为0 |
getBytes() | byte[] | 转换为字节数组 |
toCharArry() | char[] | 转换为字符数组 |
equalsIgnoreCase(String str) | boolean | 忽略大小写比较字符串是否相同 |
equals(String str) | boolean | 判断两个字符串是否相同 |
charAt(int index) | char | 得到索引上的字符 |
indexOf(String str) | int | 得到某个字符串第一次出现的索引,不存在返回-1 |
lastIndexOf(String str) | int | 得到某个字符串最后一次出现的索引,不存在返回-1 |
contains(String str) | boolean | 判断是否存在某个字符串 |
startsWith(String str) | boolean | 判断是否以指定字符串开头 |
endWith(String str) | boolean | 判断是否以指定字符串结尾 |
concat(String str) | String | 将指定字符串拼接到原字符串末尾 |
substring(int index) | String | 从索引index开始截取字符串至末尾 |
substring(int begin,int end) | String | 截取[begin,end)范围内的字符串 |
split(String regex) | String[] | 根据字符串或正则表达式切分原字符串 |
replace(String oldStr,String newStr) | String | 将原字符串中的oldStr替换为newStr |
String.valueOf(参数) | String | 将参数转换为字符串,参数可以是任何数据,通常用于原始类型转换为字符串 |
String.format(String 格 式,Object...obj) | String | 根据指定格式转换参数。常用与将浮点数据保留指定小 数位数。\n如String.format("%4.2f",2.345)表示将 2.345保留2位小数,整体占4位,输出为字符串格式。 如果实际数字总位数大于4,原样输出,如果实际数字 总位数小于4,会在最前补充空格。 |
可变字符串
String字符串对象是一个常量,在定义后,值不可改变。 如果使用String类的对象,对其频繁更新时,就会不停地创建新对象,不停引用给同一个变量。 如果要执行10000次循环重新赋值的过程,就会创建10000个字符串对象,效率很低,这是就需要使用 可变字符串对象。
public static void main(String[] args) {
System.out.println("程序开始执行");
String str = "hello";
//从1970/1/1 0:8:0 至今经过了多少毫秒
//记录开始时间
long startTime = System.currentTimeMillis();
//创建一个可变字符串对象
StringBuilder sb = new StringBuilder("hello");
//频繁更新字符串
for (int i = 0; i < 5000000; i++) {
//循环多少次,就会创建多少个String对象,每个对象的创建需要时间和空间
//str += i;//实际不是更新字符串,而是创建字符串
//全程只有一个对象参与,每次循环只是在操作该对象
sb.append(i);
}
//记录结束时间
long endTime = System.currentTimeMillis();
System.out.println("程序执行完毕");
System.out.println("用时"+(endTime-startTime)+"毫秒");
}
StringBuilder
用于表示可变字符串的一个类,是非线程安全的,在单线程环境下使用,效率更高。
StringBuffer
用于表示可变字符串的一个类,是线程安全的,在多线程环境下使用。 StringBuilder和StringBuffer中的方法都一致,只不过StringBuffer中的方法使用了synchronized关键字 修饰,表示是一个同步方法,在多线程环境下不会出现问题。
注意
String类中的所有方法调用后,都会创建一个新的String对象,即原本的String字符串不会改变
StringBuilder类中的所有方法都是在操作同一个字符串对象,每次调用方法,都会让原字符串发生 变化
StringBuilder类中没有重写equals方法,所以判断两个可变字符串对象是否相同时,如果调用 equals方法,
实际调用的是Object类中未重写的方法,即==判断。所以判断可变字符串是否相同时,需要将其转 换为String对象再调用equals方法。
可变字符串与String之间的转换
String转换为可变字符串
String str="hello";
//使用构造方法将String对象转换为StringBuilder对象
StringBuilder sb = new StringBuider(str);
可变字符串转换为String(任意类型对象转换为String)
String.valueOf(Object obj)方法
StringBuilder sb = new StringBuider("hello");
//将任意类型对象转换为String对象
String str = String.valueOf(sb);
toString()方法
StringBuilder sb = new StringBuider("hello");
//调用任意对象的toString()方法
String str = sb.toString();
拼接空字符串
StringBuilder sb = new StringBuider("hello");
String str = sb+"";
可变字符串相关面试题
比较String、StringBuilder和StringBuffer的区别
相同点:
这三个类都可以表示字符串,都提供了一些操作字符串的方法
这三个类中有相同的方法,如charAt(),indexOf()等。
这三个类都是被final修饰的类,不能被继承
不同点
String定义的字符串是一个常量。可变字符串定义的字符串是一个变量。
String类中的方法调用后,不会改变原本字符串的值。可变字符串中的方法调用后,会改变原本字 符串的值
StringBuilder是非线程安全的可变字符串类,StringBuffer是线程安全的可变字符串类,其中的方 法被synchronized修饰。
System类
这个类中包含了一些系统相关的信息和一些方法。其中的属性和方法都是静态的。
这类不能创建对象,不是因为它是以抽象类,而是因为它的构造方法是私有的。
常用方法和属性
常用方法和属性 | 属性 |
System.ou | 获取标准输出流对象,用于打印信息 |
System.in | 获取标准输入流对象,用于获取输入的信息 |
System.err | 获取错误输出流对象,用于打印异常信息 |
System.exit(0) | 终止虚拟机运行,参数0表示正常终止 |
System.currentTimeMillis | 获取从1970/1/1 0:0:0至今过了多少毫秒,long类型。通常称为时间戳 |
System.arraycopy(原数组原数组起始位置,目标数组,目标数组起始位置,复制的长度) | 将原数组中指定数量的元素复制到新数组中 |
Runtime类
runtime类的对象,表示程序运行时对象(程序运行环境对象)。
包含了程序运行环境相关的信息,常用于获取运行环境信息(如虚拟机信息),或执行某个命令
特点
这个类不是一个抽象类,但不能创建对象,因为它的构造方法是私有的
这个类提供了一个静态方法getRuntime(),通过该方法,可以获取一个Runtime类的对象。
这种方法可以保证该类只能创建一个对象,是java中的一种设计模式:单列模式。
pubilcclassRuntime{
privatestaticRuntimecurrentRuntime=newRuntime();
privateRuntime();
publicstaticRuntimegetRuntime(){
returncurrentRuntime;
}
}
使用
Date类
date日期 data数据
用于表示日期时间的类 位于java.util包下
构造方法
常用构造方法 | |
Date() | |
Date(long l) | |
Date(int year,int mouth,int,date) |
publicclassDateTest{
main() {
Datenow=newDate();
//如果直接输出Date对象,会输出星期 月份 日期 时分秒 市区 年份
//获取Date对象的对应的毫秒数
longtime=now,getTime();
//创建指定瞬间对应的日期对象
Datedate=newDate(0);
Datetoday=newDate(123,2,9);
sout(today);
}
}
常用方法
常用方法 | 作用 |
getTime() | 得到Date对应对象的毫秒数 |
after(Date when) | 判断参数是否在调用日期之后 |
befor(Date when) | 判断参数是否在调用日期之前 |
SimpleDateFormat类
用于格式化日期的类
构造方法
常用构造方法 | |
SimpleDateFormat(String pattern) | 创建一个指定日期模板的格式化日期对象 |
日期模板
特殊字符 | 作用 |
yyyy | 年 |
MM | 月 |
dd | 日 |
hh | 12小时制 |
HH | 24小时制 |
mm | 分 |
ss | 秒 |
E | 星期 |
yyyy/MM/dd HH:mm:ss E | 2023/03/09 14:05:16 周四 |
两个字母都可以写成一个 如5月 M-5,MM-55
常用方法
常用方法 | 返回值 | 作用 |
format(Date date) | String | 将Date对象按日期模板转为字符串 |
parse(String str) | Date | 将满足日期 |
SimpleDateFormat类
用于格式化日期的类。
构造方法
常用构造方法 | 作用 |
SimpleDateFormat(String pattern) | 创建一个指定日期模板的格式化日期对象 |
Canledar类
表示日历的类,包含了很多日历的相关的信息
是一个抽象类,无法创建对象,可以通过静态得到getInstrance()获取Calender类的实例
//
Calendarc=Calender.getInstrance();
System.out.println(c);
日历字段
在Calendar类中,定义了很多被final static修饰
常用方法
常用方法 | 作用 |
get(int field) | 根据日历字段获取对应的值 |
getMaximum(int field) | 获取指定日历字段的最大值,如日期最大为31 |
getActualMaximum(int field) | 获取指定日历字段的实际最大值,如11月的日期最大为30 |
getTime | 将Calendar对象转换为Date对象 |
set(int field,int value) | 将指定的日历字段设置为指定值 |
set(int year,int month,int date) | 同时设置日历的年月日 |
setTime(Date date) | 将Date对象作为参数设置日历的信息 |
使用Calendar类实现万年历
import java.util.Calendar;
import java.util.Scanner;
public class Test2 {
public static void main(String[] args) {
//实现"万年历"
//输入年份和月份,输出
Calendar c = Calendar.getInstance();
Scanner sc = new Scanner(System.in);
System.out.println("输入年份");
int year = sc.nextInt();
System.out.println("输入月份");
//日历设置为当月1号
c.set(year, month - 1, 1);
//得到当月最大日期
int maxDate = c.getActualMaximum(Calendar.DATE);
//换行计数
int count = 0;
//输出空格
/*
* DAY_OF_WEEK 星期 空格数量
* 2 一 0
* 3 二 1
* 4 三 2
* 5 四 3
* 6 五 4
* 7 六 5
* 1 天 6
* //周天空6 其余空DAY_OF_WEEK-2
* */
System.out.println("一\t二\t三\t四\t五\t六\t日");
//获取当月1号是一周中的第几天
int week = c.get(Calendar.DAY_OF_WEEK);
//周天空6格
if (week == 1) {
System.out.print("\t\t\t\t\t\t");
//空格也需要计数
count += 6;
} else {
//其他情况空星期-2个格
for (int j = 1; j <= week - 2; j++) {
System.out.print("\t");
//空格也需要计数
count++;
}
}
//输出数字
for (int i = 1; i <= maxDate; i++) {
System.out.print(i + "\t");
//计数+1
count++;
//计数到7换行
if (count % 7 == 0) {
System.out.println();
}
}
}
方法调用时传值问题
总结
参数为引用类型(类、数组、接口),且在方法中直接操作该参数时,才会对实际参数造成影响。
fun3(Person p)参数为Person对象,方法中直接调用参数p的xxx方法,是在操作实际参数。
fun5(int[] list)参数为数组,方法中在直接操作数组某个索引对应的元素,是在操作实际参数。
fun2(String str)和fun4(Person p)都在方法中创建另一个新的对象,是在操作方法中创建的对象,不影
响实际参数
public static void fun(char[] list,Person p){
list[0]='m';//方法内部直接操作实际参数,会影响实际参数
p = new Person();//方法内部创建了新对象给实参重新赋值,不会影响实际参数
p.setName("xxx");
}
public static void main(String[] args){
Person p = new Person();
p.setName("qwe");
char[] list ={'a','b','c'};
fun(list,p);
System.out.println(p.getName());//qwe
System.out.println(list[0]);//m
}
包装类
Java是纯面向对象语言,宗旨是将一切事物视为处理对象
但原始类型不属于对象,不满足面向对象的思想。但原始类型无需创建对象,保存在栈中,效率更高。
为了既保证效率又让原始类型也有对应的类类型,达到“万物皆对象”的理念,所以就有了包装类的概念
包装类就是原始类型对应的类类型
包装类常用于字符串与原始类中之间的转换
在web应用中,从浏览器页面中获取数据提交到服务器,全部都是String类型,所以一定要使用字符串转换为原始类型的方法
包装类 | 原始类型 |
Byte | byte |
Short | short |
Integer | int |
Long | long |
Float | float |
Double | double |
Character | char |
Boolean | boolean |
特点
八个原始类型中,除了int和char,其余包装类都是将原始类型的首字母改为大写
int 对应Integer char对应Charater
包装类都是被final修饰的,不能被继承
除了Charater类,其余包装类都有两个过时的构造方法,参数为对应的原始类型或字符串
Character只有一个参数为char类型的构造方法
构造方法的目的都是将原始类型的数据转换为包装类的对象
除了Charater类,其余包装类都有静态方法“parse原始类型单词(String str)”,用于将字符串转
换为相应的原始类型
数值型的包装类parseXXX()方法,如果参数不是对应的数字,就会抛出NumberFormat异
常,如"123a"或"123.4"都会报错
boolean的包装类Boolean的parseBoolean()方法,如果参数不是"true"这个单词的四个字
母,转换结果都是false
除了Boolean类,其余包装类都有MAX_VALUE和MIN_VALUE这两个静态属性,用于获取对应类型
支持的最大最小值
所有包装类都重写了toString(),用于将包装类对象转换为String对象
字符串与原始类型之间的转换
字符串转换为原始类型
使用原始类型对应的包装类,调用parseXXX(String str)方法
String num="123";
byte b = Byte.parseByte(num);//123
short s = Short.parseShort(num);//123
int i = Integer.parseInt(num);//123
long l = Long.parseLong(num);//123
float f = Float.parseFloat(num);//123.0
double d = Double.parseDouble(num);//123.0
boolean flag = Boolean.parseBoolean(num);//false
原始类型转换为字符串
使用String类的String.valueOf(Object obj)
int num=12;
String str = String.valueOf(num);
拼接空白字符串
int num=123;
String str = num+"";
将原始类型转换为包装类后,调用toString()
int num=123;
Integer integer=new Integer(num);
String str = interger.toString();
装箱和拆箱
装箱
将原始类型对象转换为包装类对象的过程称为装箱
//原本的int类型数据
int i = 123;
//包装类Integer对象调用Integer.valueOf(int i),将int数据转换为Integer对象
Integer integer = Integer.valueOf(i);
拆箱
将包装类对象转换为原始类型对象的过程称为拆箱unboxing通过“包装类对象.数据类型Value()”实现
//原本是Integer对象
Integer integer = new Integer(123);
//调用Integer类中的intValue()方法,将调用对象转换为原始int类型
int i = integer.intValue();
自动装箱和拆箱
在jdk1.5之后,为了方便原始类型和包装类之间做转换,加入了自动装箱和拆箱的概念
可以直接将原始类型和包装类对象之间互相赋值
i1和i2保存的数字在byte范围[-127,127]内,这个值会共享,只会有一个"100"对象
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1 == i2);//i1和i2引用同一个地址,结果为true
//i3和i4保存的数字不在byte范围[-127,127]内,会创建对象
Integer i3 = 128;//128对象
Integer i4 = 128;//128对象
System.out.println(i3 == i4);//i3和i4引用不同的地址,结果为false
System.out.println(i3.equals(i4));//包装类重写了equals,会拆箱后判断,结果为ture
使用自动装箱给包装类对象赋值,值的范围在byte范围[-127,127]内,这个值会保存在缓冲区
中,如果多个对象都使用这个值,共享这一个数据,使用同一个地址,==判断结果true;值的
范围不在byte范围[-127,127]内,就会创建新的包装类对象,会有不同的地址,==判断结果
false
引用类型对象比较相同时,不要使用==,包括包装类的对象。比较相同时,使用包装类重写
的equals方法
异常
当程序没有按开发人员的意愿正常执行,中途出现错误导致程序中断,这种情况,就称为异常
学习异常就是认识异常的种类,如何处理异常和避免异常,虚拟机会创建一个异常对象,如果没有对该异常对象进行处理,就会导致程序中断,不再执行后续内容
异常的产生
异常在程序中以对象的形式存在。当代码执行过程中出现异常,虚拟机会自动创建一个异常对象,如果
没有对该异常对象进行处理,就会导致程序中断,不再执行后续内容。
异常的分类
异常在程序中以对象的形式存在,就有相应的类。
所有的异常类,组成了"异常家族"。
![](https://i-blog.csdnimg.cn/blog_migrate/af16cf651fb38c77276e1e066b6e509e.png)
Error错误
如果出现xxxError,如StackOverFlowError,栈溢出,无法通过额外的代码解决,只能修改源代码。
集合和数组
集合的特点
Collection接口
该接口中有两个核心子接口:List和Set
这两个接口都可以保存一组元素,List接口保存元素有序可重复,Set接口保存元素无序不可重复
Collevtion接口还有一个父接口Iterable,它不是一个集合,而是用于遍历集合的工具接口,包含forEach()和iterable()方法
常用方法 | 返回值 | 作用 |
add(Object obj) | boolean | 将元素添加到集合中 |
size() | int | 获取集合中的元素数量 |
isEmpty() | boolean | 判断集合是否为空 |
clear() | void | 清空集合 |
contains(Object obj) | boolean | 判断集合中是否包含指定元素 |
remove(Object obj) | boolean | 移除集合中的指定元素 |
toArray() | Object[] | 将集合转换为数组 |
stream() | Stream | 获取集合中的流对象,用于遍历数组 |
Iterator() | Iterator | 得到集合的迭代器对象,用于遍历集合 |
List接口(有序可重复)
有序集合,元素可重复,允许保存Null,可以通过索引获取对应的元素
List接口在继承Collection接口后,又拓展了一些操作元素的方法
拓展方法 | 返回值 | 作用 |
get(int index) | Object | 得到指定索引的元素 |
set(int index,Object obj) | Object | 使用obj替换index上的元素,返回被替代的元素 |
add(int index,Object obj) | void | 将obj添加到指定index上 |
remove(int index) | Object | 移除指定索引的元素,返回被移除的元素 |
indexOf(Object obj) | int | 得到obj第一次出现的索引 |
lastIndexOf(Object obj) | int | 得到obj最后一次出现的索引 |
subList(int from,int to) | List | 截取[from,to)区间内的元素,返回子集合 |
ArrayList实现类(掌握)
采用数组实现的集合
可以通过索引访问元素,可以改变集合大小,如果要在其中插入或删除元素时,会影响后续元素
该集合查询效率高,中途添加和删除元素效率低
集合中保存的都是引用类型。如果集合中保存123,保存的不是int类型的123,而是Integer类型的123
构造方法
构造方法 | 说明 |
ArrayList() | 创建一个Object类型的空数组,在调用后续添加方法时,才会初始化数组大小为10 |
ArrayList(int initialCapacity) | 创建一个指定容量的Object类型数组,如果参数为负数,会抛出IllegalArhumentExcpeton异常 |
ArrayList(Collection) | 根据指定集合创建Object类型数组 |
常用方法
主要以List接口和Collection接口中的方法为主
LinkedList实现类
采用双向链表实现的集合
集合中保存的每一个元素称为节点
如果在双向链表的结构中进行插入和删除节点的操作时,不会影响其他节点现在的保存位置
添加的节点只需要记录前后节点的位置接口
如果要查询某个节点的地址时,需要从头结点或尾结点开始搜索目标节点的位置
双向链表在中间插入和删除数据效率高,随机读取的效率低
![](https://i-blog.csdnimg.cn/blog_migrate/e14bd9fb26e67819a925cbc499df9348.png)
构造方法
常用构造方法 | 说明 |
LinkedList() | 创建一个空双向链表 |
常用方法
主要以List接口和Collection接口中的方法为主。由于还实现了Deque接口,所以还有一些Deque接口中的方法。如操作首尾节点的方法。
ArrayList和LinkedList的区别
这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null
ArrayList采用数组实现,随机读取效率高,插入和删除效率低,适用于查询
LinkedList采用双向链表实现,插入和删除效率高,随机读取效率低,适用于频繁更新集合
Set接口(无序不重复)
无序集合,元素不能重复,允许保存null,没有索引
Set接口中的方法都是继承于Collection接口。
哈希表hash table
哈希表,也称为散列表,是一种数据结构,能更快速地访问数据。
![](https://i-blog.csdnimg.cn/blog_migrate/0c397b34ea4b3af33c9ecf3401a6e594.png)
假设原本的数据为左侧的数组,如要查询10,需要遍历数组,效率不高。
通过一个特定的函数"原始值%5",得到一组新数组,让新数据重新对应元素,保存到新数组中,这个新
数组称为哈希表。常用构造方法 说明
HashSet() 创建一个空集合,实际是创建了一个HashMap对象
这时如果要查询10,由于哈希函数是10%5得到0,所以直接查询哈希表中0对应的元素即可。
整个过程中,这个函数称为哈希函数,得到的新数组称为哈希码,新数组称为哈希表,对应关系称为哈
希映射。
这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实
际值不同)。
为了解决哈希冲突,可以使用"拉链法",将冲突的数据保存在对应哈希码之后的链表中。
哈希码的特点
如果两个对象的hashcode不同,这两个对象一定不同
如果两个对象的hashcode相同,这两个对象不一定相同
hashcode相同,对象不同,这种现象称为哈希冲突
“通话”和"重地"这两个字符串的hashcode相同,但是两个不同的对象
HashSet实现类
采用哈希表实现
元素不能重复,无序保存,允许保存一个null
本质是HashMap对象
使用HashSet集合时,通常要重写实体类中的equals和hashcode方法
构造方法
常用构造方法 | 说明 |
HashSet() | 创建一个空集合,实际是创建了一个HashMap对象 |
常用方法
HashSet中没有定义属于自定的方法,都是父接口Set和Collection中的方法。
HashSet添加数据的原理
如果添加的两个元素的equals方法结果为true且hashcode相同时,视为同一个对象,不能添加。
每次向集合中添加元素时,先判断该元素的hashcode是否存在
如果不存在,视为不同对象,直接添加
如果存在,再判断equals方法的结果
如果true,视为同一个对象,不能添加
如果false,视为不同对象,可以添加
由此可见,不能添加的条件是两个对象的hashcode相同且equals的结果为true。
可以只判断equals的结果。但是如果每次都判断equals,由于重写equals时会判断很多属性,效率不
高。
如果只判断hashcode是否相同,效率高,但可能会出现哈希冲突
所以先判断hashcode,再判断equals,既能保证效率,又能保证不会添加重复元素。
equals和hashcode的关系
如果两个对象的equals方法结果为true,在没有重写equals方法的前提下,hashcode相同吗?
如果没有重写equals方法,默认使用Object中的方法,使用==判断,如果结果为true,说明
是同一个地址,同一个对象,hashcode一定相同
如果两个对象的hashcode不同,在没有重写equals方法的前提下,equals方法的结果为?
hashcode不同,说明不是同一个对象,没有重写equals,说明使用Object中的方法,使用==
判断,结果为false
如果两个对象的hashcode相同,equals方法的比较结果为?
可能为true或false
String str1="hello";
String str2="hello";
//str1和str2使用同一个地址,hashcode相同,equals结果为true
String str3="通话";
String str4="重地";
//str3和str4不是同一个地址,但hashcode相同,这种情况称为哈希冲突,equals结果为
false
HashSet的应用
如果要保存的对象保证不重复,且无关顺序,可以使用HashSet。重写要保存的元素的equals和
hashcode方法。
Student类,保证添加对象时,不重复
public class Student {
private int id;
private String name;
private String major;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", major='" + major + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && Objects.equals(name, student.name) &&
Objects.equals(major, student.major);
}
}
/*
TreeSet实现类
特殊的Set实现类,数据可以有序保存,可以重复,不能添加null
采用红黑树(自平衡二叉树)实现的集合
二叉树表示某个节点最多有两个子节点
某个节点右侧节点值都大于左侧节点值
红黑树会经过"变色"和旋转达到二叉树的平衡
只能添加同一种类型的对象且该对象实现了Comparable接口
每次调用add方法添加元素时,会自动调用Comparable接口中的方法compareTo()方法
实现Comparable接口后必须要重写compareTo()方法,用于决定新添加的元素放在旧元素之
前或之后
compareTo方法的返回值决定了能否添加新元素和新元素的位置
如果返回0,视为每次添加都是同一个元素,不能重复添加
如果返回正数,将新元素添加到现有元素之后
如果返回负数数,将新元素添加到现有元素之前
添加的元素可以自动排序
构造方法
常用构造方法 | 说明 |
TreeSet() | 创建一个空集合 |
常用方法
能使用Set和Collection接口中的方法,还定义了一些属于它的方法
独有方法 | 作用 |
first() | 得到集合中的第一个元素 |
last() | 得到集合中的最后一个元素 |
ceiling(Object obj) | 得到集合中比参数大的元素中的最小元素 |
floor(Object obj) | 得到集合中比参数小的元素中的最大元素 |
TreeSet的应用
如果要保存的元素需要对其根据某个属性排序,使用该集合。
如在集合中保存整数,即Integer对象,Integer类已经实现了Comparable接口,
如要保存自定义的元素,必须要实现Comparable接口,重写compareTo方法,自定义排序规则。
Map接口
Map称为映射,该集合中保存的数据是以键值对的形式保存,保存的键与值的映射关系
键称为Key,值称为Value,键不能重复,允许出现一个null作为键,值没有限制
键和值都必须是引用类型
如yyds --永远都是 这就是一个映射关系,yyds是键 ,后面的是值
常用方法 | 作用 |
size() | 得到键值对的数量 |
isEmpty | 判断是否为空集合 |
clear() | 清空所有键值对 |
put(Object key,Object value) | 添加一组键值对 |
get(Object key) | 根据键得到对应的值 |
containsKey(Object key) | 判断是否存在某个键 |
containsValue(Object value) | 判断是否存在某个值 |
keyset() | 得到键的集合 |
values() | 得到值的集合 |
entrySet() | 得到键值对的集合 |
remove(Object key) | 删除指定的键值对 |
HashMap实现类
构造方法
常用构造方法 | 说明 |
HashMap() | 创建一个大小为16,加载因子为0.75的空集合 |
常用方法
使用Map接口中的方法
![](https://i-blog.csdnimg.cn/blog_migrate/c20862944ed27ba95ee62aa2bc3bcce0.png)
JDK1.8之后,HashMap采用"数组+链表+红黑树"实现
当没有出现哈希冲突时,元素保存在数组中
如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
如果链表的长度大于8,将链表转换为红黑树
遍历集合的方式
遍历List集合
普通for循环
for (int i = 0; i < 集合.size(); i++){
元素 变量 = 集合.get(i);
}
增强for循环
for(数据类型 变量名 : 集合){
元素 变量 = 集合.get(i);
}
forEach()方法
使用该方法遍历集合时,不要使用add或remove操作,遍历会抛出异常。
集合.forEach(obj -> {
元素 变量 = 集合.get(i);
});
迭代器
//Collection接口有一个父接口Iterable,其中有一个iterator方法用于获取迭代器对象遍历集合
//所有Collection的子实现类都能调用该方法
Iterator it = Collection集合.iterator();
//hasNext()判断是否还有下一个元素
while(it.hasNext()){
//next()方法读取该元素
元素 变量 = it.next();
}
遍历Set集合
普通for循环无法遍历Set集合,因为元素没有索引
增强for循环
遍历HashMap集合
泛型
一种规范,常用于限制集合中的元素类型,省去遍历集合时转换Object对象的过程
//默认可以保存任意类型的元素,即Object类型
List list = new ArrayList();
list.add(123);
list.add("hello");
//遍历时只能使用Object类型获取
for(Object obj : list){
}
使用泛型
在定义集合时,在集合类或接口后写上<引用数据类型>
集合类或接口<引用数据类型> 集合遍历名 = new 集合实现类();
//定义只能保存整数的集合,要使用整数的包装类类型
List<Integer> list = new ArrayList();
list.add(123);
//不能添加非整数
//list.add("hello");
Collections集合工具类
Collection是集合的根接口,定义了操作集合中元素的方法。
Collections是集合的工具类,定义了操作集合中元素的静态方法。
Collections中的静态方法 | 说明 |
Collections.shuffle(List list) | 打乱集合中元素的顺序 |
Collections.sort(List list) | 对集合中的元素进行排序。元素必须实现Comparable接口。 |
Collections.swap(List list,int a,int b) | 将集合中索引a和b的元素交换位置 |
Collections.reverse(List list) | 反转集合中的元素 |
Collections.max(Collection c) | 得到集合中元素的最大值。元素必须实现Comparable接口。 |
Collections.rotate(List list,intdistance) | 将集合中最后distance个元素放在集合最前 |
Collections.fill(List list,Object obj) | 使用obj填充集合 |
Arrays数组工具类
包含了一些操作数组的静态方法
常用静态方法 | 说明 |
Arrays.sort() | 对数组中的元素升序排序 |
Arrays.asList(T... obj) | 将可变参数转换为ArrayList集合 |
集合和数组之间转化
数组转换为集合
//调用Arrays工具类的asList(1,2,6,22,11)或asList(数组)
ArrayList<Integer> list = Arrays.asList(1,2,6,22,11);
集合转换为数组
ArrayList list = new ArrayList();
list.add("sdf");
list.add(123);
list.add(null);
//调用集合的toArray()方法
Object[] objs = list.toArray();
无论是数组转换集合还是集合转换数组,都可以进行遍历。
如果集合转换为数组,遍历集合,通过索引赋值。
如果数组转换为集合,遍历数组,通过add()添加元素
TreeSet实现类
常用方法
能使用Set和Collection接口中的方法,还定义了一些属于它的方法
常用方法 | 作用 |
如果要保存的元素需要对其根据某个属性排序,使用该集合
文件File
java中的File类,表示本地硬盘中的文件file或文件夹directory的一个类
通过这个类创建对象,可以读取文件信息或操作对应文件
构造方法
构造方法 | 作用 |
File(String pathName) | 根据文件的完整路径,创建File对象 |
File(String parent,String child) | 根据文件的父目录的路径和自身文件名称获取文件 |
File(File parent,String child) | 根据父目录的File对象,和自身文件夹的名称获取文件 |
//使用不同构造方法表示C:\Users\ASUS\Desktop\1.txt 文件
//new File(文件完整路径)
File file1 = new File("C:\\Users\\ASUS\\Desktop\\1.txt");
//new File(父目录路径,文件名称)
File file2 = new File("C:\\Users\\ASUS\\Desktop","1.txt");
//new File(父目录的File对象,文件名称 )
File parent = new File("C:\\Users\\ASUS\\Desktop");
File file3 = new File(parent,"1.txt");
常用方法
常用方法 | 返回值 | 作用 |
exists() | boolean | 判断文件是否存在 |
isFile() | boolean | 判断是否是文件 |
isDirectory() | boolean | 判断是否是文件夹 |
getName() | String | 获取文件名 |
getPath() | String | 获取文件路径 |
getAbsoluteFile() | String | 获取绝对路径 |
getParent() | 获取父目录路径 | |
getParentFile() | 获取父目录对象 | |
length() | 获取文件大小 | |
lastModified() | 最后一次修改时间毫秒数 | |
isHidden() | 查看文件是否为隐藏文件 |
Io
I:input输入
O:output输出
流Stream
在Java中,流表示计算机硬盘和内存之间传输数据的通道
将内存中的数据存入到硬盘中,称为写write,也称为输出Output
将硬盘中的数据存入到内存中,称为read,也称为输入Input
![](https://i-blog.csdnimg.cn/blog_migrate/62ec50f55eab4d7d6816479c352fe6c4.png)
流的分类
java中的流也是类,以对象的实现表示流,流有“四大家族”,是所有流的父类
字节输入流InputStream
FileInputStream ObjectInputStream
字节输出流
FileOutputStream ObjectOutputStream
字符输入流Reader
FileReader、BufferedReader、inputSteamReader
字符输出流Writer
FileWriter、BufferedWriter、inputSteamWriter
按方向分类
输入流:InputStream、Reader
读硬盘中的数据到程序中
输出流:OutputStream、Writer
将程序中的数据写到硬盘中
按数据传输类型分类
字节流:InputStream、OutputStream
读取非文本类型文件,如图片、
字符流:Reader、Writer
读写纯文本文件,如txt、md等
如要将硬盘中的某个txt文件中的内容读取到程序中,使用Reader
如要将硬盘中的某个图片文件中的内容读取到程序中,使用InputStream
如要将程序中的文本写入到硬盘中,保存为txt文件时,使用Writer
如要将程序中的数据写入到硬盘中,保存为非文本文件时,使用OutputStream
流的四个父类的特点
这四个父类都是在java.io包下,都是抽象类,不能直接创建其对象,使用其子类对象
这四个类都有close()方法,用于关闭流对象,释放资源
输入流(InputStream和Reader)都有read()方法,用于读取数据,输出流(OutputStream和Writer)都有write()方法
输出流(OutputStream和Writer)都有flush()方法,用于将流中的数据冲刷到硬盘中
在使用输出流对象时,一定要调用flush()或close()方法后,才能真正将数据写入到硬盘中
所有流中,以Stream结尾,都是字节流,数据以字节传输;以Reader或Writer结尾,都是字符流,数据以字符传输
读取硬盘中的数据时,读取的文件必须存在;写入数据到硬盘中时,写入的文件可以不存在,但父目录必须存在。
FileInputStream文件字节输入流(掌握)
以字节的形式读取文件
构造方法
常用构造方法 | 作用 |
FileInputStream(String filePath) | 根据文件完整路径创建流对象 |
FileInputStream(File file) | 根据文件对象创建流对象 |
常用方法
常用方法 | 作用 |
read() | 读取一个字节,返回读取到的字节 |
read(byte[] bytes) | 读取指定数组大小的字节,返回读取到的字节数量 |
read(byte[] bytes,int off,int len) | 读取指定数组大小的字节,从off索引开始读取len个字节,返回读取到的字节数量 |
available() | 文件可读取的最大字节数量 |
close() | 关闭流对象 |
FileOutputStream文件字节输出流(掌握)
以字节的形式写入文件
构造方法
常用构造方法 | 说明 |
FileOutputStream(String filePath) | 根据文件名创建流对象,覆盖写入 |
FileOutputStream(String filePath,boolean appen) | 根据文件名创建流对象,追加写入 |
FileOutputStream(File file) | 根据文件对象创建流对象,覆盖写入 |
FileOutputStream(File file,boolean appen) | 根据文件对象创建流对象,追加写入 |
常用方法
常用方法 | 作用 |
write(int b) | 写入一个字节 |
write(byte[] bytes) | 写入一个字节数组 |
write(byte[] bytes,int off,int len) | 写入一个字节数组,从off开始的len个字节 |
flush() | 将流中的数据冲刷到硬盘中 |
close() | 关闭流对象 |
使用FileInputStream和FileOutputStream读写时的注意事项
在通过FileInputStream对象使用read(byte[] bytes)方法时,每次读取指定数组的字节,将读取到的字节保存在字节数组中。
该方法的返回值是读取到的字节数量,如果最后一次读取的字节数量不足字节数组的大小时,只会将读取到的内容覆盖数组中最前的几个元素。
所以最后一次读取时,读取的内容多于实际内容。
在通过FileOutputStream对象使用write(byte[] bytes),会将字节数组中的所有内容写入到输出流中,在最后一次写入时,会写入多余的内容。
所以在写入时,使用write(byte[] bytes,int off,int len)方法,表示将字节数组中的内容,从off开始写入len个
//如有test.txt中aaabbbccc
//读取
FileInputStream fis=new FileInputStream("test.txt");
byte[] bytes=newbytes[4];
intcount=0;
//第一次读取到aaab,读取了4个字节,当前数组为aaab
count=fis.read(bytes);
//第一次读取到bbcc,读取了4个字节,当前数组为bbcc
count=fis.read(bytes);
//第一次读取到c,读取了1个字节,当前数组为cbcc
count=fis.read(bytes);
//写入
FileOutputStream fos=new FileOutputStream("copy.txt");
//如果使用fos.write(byte[]);方式,最后一次会写入多于的bcc
fos.write(bytes);
//如果使用fos.write(byte[],int,int);方式,最后一次只会写入c
fos.write(bytes,0,count);
单文件复制
public class Test2 {
public static void main(String[] args) throwsIOException {
//使用FileInputStream读取字节的同时使用FileOutputStream写入字节,实现单文件复制
//源文件
Filesource=new File("d:\\xxx.txt");
//读取
FileInputStream fis=new FileInputStream(source);
//写入到目标文件中
FileOutputStream fos=new FileOutputStream("f:\\copy.txt");
//定义字节数组,大小为8MB
byte[] bytes=newbyte[1024*1024*8];
//读取指定数组大小的字节,返回读取到的字节数量
intcount=fis.read(bytes);
while (count>0) {
//写入指定数组的字节
//fos.write(int b);写入一个字节
//fos.write(byte[] bytes);写入一个数组的字节
fos.write(bytes,0,count);//写入一个数组的字节,从0开始写入读取到的数量的字节
count=fis.read(bytes);
}
fis.close();
fos.close();
}
}
复制文件夹
public class Test3 {
public static void main(String[] args) throwsIOException {
//复制文件夹
Filefile=new File("C:\\Users\\Administrator\\Desktop\\测试");
File target=new File("f:\\复制文件夹");
copyDir(file,target);
}
/*
* 复制单个文件
* */
public static void copyFile(Filesource,Filetarget) throws IOException {
//读取源文件
FileInputStream fis=new FileInputStream(source);
//写入新文件
FileOutputStream fos=new FileOutputStream(target);
//读写字节数组
byte[] bytes=newbyte[1024*1024*8];
//尽可能按数组大小读取
intcount=fis.read(bytes);
//如果能读到,写入
while(count>0){
fos.write(bytes,0,count);
count=fis.read(bytes);
}
fis.close();
fos.close();
}
/*
* 复制文件夹
* */
public static void copyDir(Filesource,Filetarget) throws IOException {
//如果是文件夹
if (source.isDirectory()) {
//创建目标文件夹,可以与源文件夹同名,也可以使用参数target名
target.mkdir();
//展开源文件,得到第一层子文件
for (Filechild : source.listFiles()) {
//需要将所有子文件保存在上一步创建的文件夹中
//使用构造方法File(File parent,String child),定义新目标文件的路径
//如source为桌面的"桌面/测试/a",复制到f盘的“复制文件夹”中,递归调用的target就为"f:/复制文件夹/a"
File newTarget=new File(target, child.getName());
//递归调用,这时的源文件为遍历出的子文件,目标文件为上一步构造的对象
copyDir(child,newTarget);
}
}else{
//如果是文件
copyFile(source,target);
}
}
}
FileReader文件字符输入流
按字符读取文件
构造方法
常用构造方法 | 说明 |
FileReader(String fileName) | 根据文件名创建文件字符输入流对象 |
FileReader(File file) | 根据文件对象创建文件字符输入流对象 |
常用方法
常用方法 | 作用 |
ready() | 判断是否还有下一个字符 |
read() | 读取下一个字符,返回读取到的字符 |
read(char[] chars) | 按字符数组读取,返回读取到的字符数量,读取到的字符保存在字符数组中 |
close() | 关闭流对象 |
BufferedReader缓冲字符输入流(掌握)
自带缓冲区(字符数组)的字符输入流。默认字符数组大小为8192,每次最多读取8192个字符。
在读取纯文本文件(txt或md)时,首选该类。
构造方法
常用构造方法 | 作用 |
BufferedReader(Reader in) | 创建一个带有缓冲区(大小为8192的char数组)的字符输入流对象,参数为Reader类型对象,Reader是抽象类,所以实际参数为Reader的子类,如FileReader,在FileReader对象中定义要读取的文件 |
BufferedReader(Reader in,int size) | 创建一个指定缓冲区(字符数组)大小的字符输入流对象 |
常用方法
常用方法 | 作用 |
ready() | 判断是否还有字符 |
readLine() | 读取整行字符 |
close() | 关闭流对象 |
FileWriter文件字符输出流
按字符写入文件。必须在调用flush()或close()方法后才会写入。
构造方法
常用构造方法 | 说明 |
FileWriter(String filePath) | 根据文件路径创建流对象,覆盖写入 |
FileWriter(String filePath,boolean append) | 根据文件路径创建流对象,append为true时追加写入 |
FileWriter(File file) | 根据文件对象创建流对象,覆盖写入 |
FileWriter(File file,boolean append) | 根据文件对象创建流对象,append为true时追加写入 |
常用方法
常用方法 | 作用 |
writer(String str) | 写入字符串 |
flush() | 冲刷数据到硬盘 |
close() | 关闭流对象 |
BufferedWriter缓冲字符输出流(掌握)
按字符写入,带有缓冲区的输出流。必须在调用flush()或close()方法后才会写入。
构造方法
常用构造方法 | 说明 |
BufferedWriter(Writer writer) | 根据Writer对象创建字符缓冲输出流 |
常用方法
常用方法 | 作用 |
writer(String str) | 写入字符串 |
newLine() | 写入空白行 |
flush() | 冲刷数据到硬盘 |
close() | 关闭流对象 |
序列化ObjectOutputStream对象字节输出流(掌握)
序列化:将对象转换为文件的过程
被序列化的对象,其类必须要实现Serializable接口
这个接口是一个特殊的接口,其中没有定义任何方法,只是给类加上标记,表示该类可以被序列化
构造方法
构造方法 | 说明 |
ObjectOutputStream(OutputStream os) | 根据字节输出流对象创建 |
常用方法
常用方法 | 作用 |
writeObject(Object obj) | 将一个对象写入到本地文件 |
flush() | 冲刷数据到硬盘 |
close() | 关闭流对象 |
反序列化ObjectInputStream对象字节输入流(掌握)
反序列化:将文件转换为对象的过程
构造方法
构造方法 | 说明 |
ObjectInputStream(InputStream is) | 根据字节输入流对象创建 |
常用方法
常用方法 | 作用 |
readObject() | 读取序列化后的文件,返回Object类型 |
close() | 关闭流对象 |
序列化和反序列化案例
Person类,实现Serializable接口
/*
* 如果要序列化某个类的对象,该类必须要实现Serializable接口
* 该接口中没有定义任何方法,只是一个标记接口,表明该类可序列化
* */
public class Person implements Serializable {
private String name;
private int age;
private String sex;
//省略get/set/构造方法等
}
测试类
packagecom.hqyj.ObjectStreamTest;
importjava.io.*;
importjava.util.ArrayList;
public class Test {
public static void main(String[] args) throwsIOException, ClassNotFoundException {
Personp1=new Person("王海", 22, "男");
Personp2=new Person("刘玉梅", 29, "女");
Personp3=new Person("白伟强", 21, "男");
//将程序运行中的数据(对象)保存到本地,这个过程就称为序列化
Array List<Person> list=new ArrayList<>();
list.add(p1);
list.add(p3);
list.add(p2);
//创建 "对象输出字节流" 对象,参数为字节输出流对象,这里使用FileOutputStream子类
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("person.p"));
oos.writeObject(list);
oos.close();
//创建一个对象,将其保存为一个文件,再读取该文件中保存的对象
//ObjectInputStream 反序列化
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("person.p"));
//读取序列化文件中的对象
Objectobj=ois.readObject();
//需要转换为相应的类型后使用
ArrayList<Person> pList= (ArrayList<Person> ) obj;
for (Personperson : pList) {
System.out.println(person);
}
oos.close();
ois.close();
}
}
InputStreamReader
属于转换流(将字节流转换为字符流)
OutputStreamWriter
属于转换流(将字节流转换为字符流)
案例参考网络编程套接字通信
可变参数
当某个方法的参数是同一种类型且数量未知时,参数列表中可以使用可变参数定义。
修饰符返回值类型 方法名(数据类型... 形参名){
//这时的参数的数量是可变的,[0,∞)
//整个参数是一个数组
}
特点
可变参数只能出现一次,且是最后一个参数
可变参数实际是一个数组
调用参数为可变参数的方法时,传递的实参要使用逗号隔开
public class Main {
/*
* 计算一些数字的总和
* */
public int getSum(int... nums) {
int sum=0;
for (intnum : nums) {
sum+=num;
}
return sum;
}
public static void main(String[] args) {
//
Main main=new Main();
//调用可变参数的方法,实参之间使用逗号隔开
System.out.println(main.getSum(1,2,3,4));
}
}
枚举
Java中的枚举是一个特殊的类,是一些常量的集合。
如星期可以用数字1-7表示,也可以用"周一到周天"表示,
也可以用SUN,MON,TUE,WED,THU,FRI,SAT这些来表示。
这三种都可以称为枚举,对星期这个概念进行枚举。
定义枚举类型
public enum枚举类型名{
常量1,常量2...常量N
}
使用枚举类型
内部类
内部类,是指定义在类中的类。
内部类通常使用private修饰,定义在类中,用于隐藏类的实现细节,将类进行封装
内部类分为:成员内部类,静态内部类,局部内部类和匿名内部类。
匿名内部类
没有名称的内部类称为匿名内部类。
当某个方法的参数为接口或抽象类对象时,通常会先定义接口的实现类或抽象类的子类,再创建子类对象作为方法的参数。
如果使用匿名内部类,就可以不用创建子类,而是直接通过匿名内部类作为参数使用。
Lambda表达式
JDK8中的核心升级点。
通常用于简化匿名内部类的写法。
要简化的匿名内部类必须是函数式接口(只有一个抽象方法的接口)。
如上方的案例中,USB接口中只有一个抽象方法start()。
可以简化为
public class Main{
public static void main(String[] args){
Computercomputer = new Computer();
//使用lambda表达式后,简化匿名内部类的写法
computer.powerOn(() -> {
System.out.println("某USB设备接入");
});
}
}
Lambda表达式语法
(参数类型 参数名) -> {代码语句;}
小括号部分表示参数列表
->部分表示要执行什么
{}部分表示执行的内容
使用lambda表达式遍历集合
面向过程和面向对象编程思想
面向过程POP:所有步骤按顺序执行,注重执行的细节。
面向对象OOP:创建解决问题的对象,让各个对象调用各自的方法而配合完成。
在面向对象编程OOP中,给类中定义的方法,具体的实现过程,其实也是面向过程的。
对象和类
对象Object
某个类的具体实例
类Class
是拥有相同属性和行为的对象的集合。是对象的模板。
定义类
修饰符class类名{
//属性:定义变量
//行为:定义方法
}
类中的属性
成员变量、局部变量、静态常量
成员变量:定义在类中的变量,即类的属性。它有默认值。通过对象访问。
局部变量:定义在方法中的变量。它没有默认值。只能在方法中赋值后才能使用。
静态常量:特殊的成员变量,用final static修饰。它有默认值。通过类名访问。
类中的方法
构造方法
成员方法
静态方法
class Person{
//成员变量
String name;
//静态常量
final static String COUNTRY="中国";
//构造方法
public Person(Stringname){
this.name=name;
}
//成员方法
void info(){
//局部变量
String address="悉尼";
System.out.println("我叫"+name+",来自于"+COUNTRY+",现居住"+address);
}
//静态方法
public static void fun(){
}
}
创建对象
类名 变量名=new构造方法([参数]);
构造方法
是一种特殊的方法。方法名和类名一致,没有返回值部分。
访问修饰符 类名(数据类型 参数名称){
}
没有返回值部分,方法名必须和类名一致。
在使用new关键字创建对象时,调用对应的构造方法。
每个类在定义后,都隐藏有一个无参的构造方法。
如果自定义了有参数的构造方法,无参数的构造方法就会失效。如果想要使用无参构造方法,就要再写出来。
构造方法通常用于限制创建对象时携带的参数,初始化成员变量。
构造方法之间都是重载关系,构造方法不能重写。
构造方法执行时,不一定会创建对象。如抽象类中有构造方法,但无法创建抽象类对象,只能在创建抽象类的子类对象时,自动调用抽象类的构造方法。
面向对象三大特性
封装
将类中的属性使用private修饰,这样就能防止非当前类对其访问,隐藏类中的关键属性。
通常会对private修饰的属性提供公开的get和set方法用于获取和赋值。
继承
类A extend 类B。
接口A extends 接口B。
前者就是后者的子类,前者的对象就能直接访问后者中非私有成员。
重写Override
子类继承父类后,对父类中的非私有方法进行重写,达到拓展或重做的目的
必须满足方法名、返回值、参数列表都相同
访问权限不能比父类中的方法更严格
不能抛出比父类中的方法更大的异常
重载Overload
在一个类中,某个方法在不同的参数下,表现不同的行为。同名不同参。
方法名必须相同
参数列表必须不同(类型和数量)
无返回值无关
this和super
都可以当做对象后构造方法使用。
当做对象:this表示当前类的对象,super当前类的父类对象。
this或super当做对象使用时,只能用在非静态方法中。
当做构造方法
this()表示当前类中无参构造方法,如果带参数就表示对应参数的构造方法
super()表示当前类的父类的无参构造方法,如果带参数就表示对应参数的构造方法
this()或super()只能用在另一个构造方法的首行。
在继承后,如果子类和父类都没有写出任何构造方法时,子类中有一个隐藏的无参构造方法,会自动调用父类的无参构造方法
所以如果在父类中定义了有参数的构造方法,无参构造方法就会失效,子类必须调用父类中的有参数的构造方法
Object类
是所有类的父类。任何类都间距地继承了Obejct类。所以所有类都能访问Object类中的方法,都可以进行重写。
toString()是Object类中的一个方法,在输出对象时自动调用。
默认输出"类全限定名@十六进制哈希码"。通常在自定义的实体类中,重写toString(),输出当前类的属性。
equals()是Object类中的一个方法,用于比较两个对象是否相同。
默认使用==比较内存地址。通常在自定义的实体类中,重写equals(),自定义比较的规则。
hashCode()是Object类中的一个方法,用于获取对象的哈希码。
默认使用全部参数生成哈希码。可以重写自定义生成哈希码的参数。
对象转型
类似于原始类型间的数据类型转换
向上转型:父类变量 = 子类对象;
类似于自动类型转换
Personp=new Person();
Object obj=p;
向下转型: 子类变量 =(子类) 父类对象;
类似于强制类型转换
Animal anl=new Animal();
Cat c= (Cat) anl;
多态
在继承关系中,子类的对象可以保存到父类的变量中。(向上转型)
多态常用与定义方法时,形参为一个父类或接口类型变量,实参为子类对象。
无法通过父类变量调用子类中独有的方法。如果调用重写了父类中的方法时,执行重写后的内容。
父类/父接口对象 =new子类();
修饰符
访问修饰符
当前类 | 当前包 | 不同包中的子类 | 不同包中的非子类(同一个项目中的不同类) | |
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
不写 | √ | √ | × | × |
private | √ | × | × | × |
final
修饰属性
变量变为常量,定义时就要赋值,程序运行过程中不能改变其值。
修饰方法
方法成为最终方法,不能重写。
修饰类
类为最终类,不能被继承。
abstract
修饰方法
方法为抽象方法,没有方法体。同时所在的类也需要使用abstract定义为抽象类
修饰类
抽象类不能创建对象
抽象类除了能定义抽象方法外,其余与普通类无区别
抽象类中可以有构造方法,在创建其子类对象时自动调用
抽象类通常需要被继承,一旦有子类继承,子类就要重写抽象父类中的所有抽象方法,或者子类也是一个抽象类
interface
用于定义接口的关键字。代替class,能让java实现"多继承"。
如果某个抽象类中的所有方法都是抽象方法时,可以将该类改为接口。
abstract class --> interface
接口是一个完全抽象类,其中的方法都是public abstract修饰的抽象方法,其中的属性都是public final static修饰的静态常量。
接口中没有构造方法,不能创建对象
接口通过implements“继承”。类A implements 接口A,接口B,称为类A实现了接口A和接口B,一个类可以"继承"多个父接口
一个类一旦实现了某个接口,就要重写其中的所有抽象方法
JDK1.8后,可以在接口中定义default或static修饰的方法,该方法不用重写。
static
被static修饰的内容称为静态成员
静态成员在类加载时就会保存到内存中,所以访问时通过类名直接访问。
当某个属性或方法被高度重用时,将其定义为静态的,之后通过类名方便调用。
修饰方法
修饰属性
定义代码块
进程和线程
进程Process
进程就是操作系统中正在执行的程序。
一个程序就是一个执行的进程实体对象
每个运行的进程,都有属于它独立的内存空间,各个进程之间互不影响。
线程Thread
线程是一个进程中的执行单元,一个进程中可以有多个线程
多个线程之间可以访问同一个进程的资源
每个线程都有一块独立的栈空间,这些线程所在的栈空间位于同一进程空间中
多线程
如果一个进程中,同时执行着多个线程,就称为多线程
多线程可以提高程序执行效率,如多个窗口卖票,就可以加快卖票的效率
其实每个执行的java程序,都是多线程执行,main方法所在的线程为主线程,还有gc线程(守护线程)在同时运行
如工厂有很多车间,每个车间有很多流水线
工厂就是内存,车间就是进程,每个流水线就是进程中的一个线程
并行和并发
并行
各个进程同时执行,称为并行
并发
多个线程同时执行,称为并发
同步和异步
同步
所有任务排队执行,称为同步执行
异步
在执行任务A的同时,执行任务B,称为异步执行。
Java中的线程Thread类
java中,线程以对象的形式存在
Thread类表示线程类
获取线程对象
获取当前正在运行的线程对象
Thread thread = Thread.cuurenrThread();
创建一个线程对象
常用构造方法 | 说明 |
Thread() | 创建一个线程对象 |
Thread(String name) | 创建一个指定线程名的线程对象 |
Thread(Runnable target) | 根据Runnable对象创建线程对象 |
Thread(Runnable target,String name) | 根据Runnable对象和指定线程名的线程对象 |
线程类的常用方法
常用方法 | 作用 |
getId() | 获取线程Id |
getName() | 获取线程名 |
getPriority() | 获取线程优先级,默认5 |
getState() | 获取线程状态 |
setName(String name) | 设置线程名 |
setPriority(int i) | 设置线程优先级(1-10),数字越大优先级越高,越先执行 |
isDaemon() | 判断该线程是否为守护线程 |
setDaemon(boolean on) | 设置为守护线程 |
start() | 让线程进入就绪状态,等待执行 |
run() | 在线程获得执行权时要执行的方法(线程要做的事情) |
Thread.sleep(long m) | 让线程休眠m毫秒后继续执行 |
Thread.currentThread() | 获取当前执行的线程对象 |
实现多线程
方式一:继承Thread类
创建一个类,继承Thread类
重新Thread类中的run()方法,该方法表示当该线程执行时要做的事情
创建i自定义的线程对象,调用start()方法,让线程就绪
自定义Thread类的子类
public class MyThread extends Thread{
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
]
main类(主线程)
public static void main(String[] args) {
//创建自定义的线程对象,调用start()方法让线程就绪
new MyThread().start();
//主线程同样打印1-100
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
方式二:实现Runnable接口(建议使用)
由于Java中是单继承,如果某个类已经使用了extends继承了另一个类,就无法再继承Thread类
这时就可以让自定义线程类实现Runnable接口
自定义一个类,实现Runnable接口
重写run()方法,将多线程要执行的内容写在该方法中
创建Runnable接口的实现类对象
使用Thread(Runnable target)构造方法,将Runnable接口的实现类对象包装为Thread对象
方式三:使用匿名内部类
如果不想创建Thread的子类或Runnable的实现类,就可以将匿名内部类当做Runnable接口的实现类来使用。甚至可以配合lambda表达式更进一步简化。
public class Test {
public static void main(String[] args) {
// new Thread(Runnable target)参数为接口类型,可以使用匿名内部类实现
//使用匿名内部类实现多线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
},"线程A");
t1.start();
//使用Lambda表达式简化匿名内部类
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
},"线程B").start();
}
}
线程生命周期
线程的初始化到终止的整个过程,称为线程的生命周期
![](https://i-blog.csdnimg.cn/blog_migrate/5d335d459386bb0bf12720ff42c2e3d9.png)
新生状态
当线程对象创建成功后,进入新生状态
就绪状态
当某个线程对象调用了start()方法后,进入就绪状态
在这个状态下,线程对象不会做任何事情,只是在等待CPU分配时间片
运行状态
当某个线程对象得到CPU时间片(CPU执行该线程时分配的时间),进入运行状态,开始执行run()方法
不会等待run()方法执行完毕,只会在指定的时间内,尽可能地执行run()方法。
如果多线程下,某个线程对象在调用完run()方法后,就会再进入就绪状态。
阻塞状态
如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞。
sleep()方法,在指定时间后,会让线程重新就绪。
wait()方法只有在被调用notify()或notifyAll()方法将其唤醒后才能重新就绪
终止状态
当某个线程的run()方法中的所有内容执行完,就会进入终止状态。
守护线程
如果将一个线程设置为setDeamon(true),表示该线程为守护线程。
守护线程会随着其他非守护线的结束而结束。
public class DaemonTest {
public static void main(String[] args) {
//将含有死循环的线程设置为守护线程
Thread t1 = new Thread(()->{
while (true) {
System.out.println("守护线程运行中。。");
}
});
//设置为守护线程
t1.setDaemon(true);
t1.start();
/*
* 普通线程,有限循环
* */
new Thread(()->{
while (true) {
for (int i = 0; i < 100; i++) {
System.out.println("普通线程运行中。。");
}
}
}).start();
//随着有限循环结束,死循环进程也会结束
//非守护线程结束,守护线程也会结束
}
}
多线程访问同一个资源
可能会出现的问题
出现问题的原因
由于线程调用start()方法后,就会进入就绪状态,当该线程获得了CPU时间片,就开始执行run()方法
并不会等待run()执行完毕,所以很有可能在线程A执行run()方法的途中,线程B也开始执行run()方法,造成数据共享时出错
![](https://i-blog.csdnimg.cn/blog_migrate/2cfe1acdad7bf6e6a8aa5c6fce943a57.png)
如何解决
如果要防止在线程A执行途中线程B插队的情况,可以让所有线程同步(排队)执行
这样一来,某个线程执行run()方法的时候,其他线程就会等待run()执行完毕
synchronized关键字
这个关键字可以修饰方法或代码块
修饰方法
写在方法的返回值前,这时该方法就称为同步方法
public synchronized void sell(){
//该方法执行途中,其他线程无法插队
}
修饰代码块
synchronized(要同步的对象){
//该方法执行途中,其他线程无法插队
}
原理
每个对象默认都有一个“锁”,当某个线程运行到被synchronized修饰的方法时
该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其他执行结束后,才会释放这把锁
使用synchronized修饰后的锁称为“悲观锁”
方法被synchronized修饰后,称为同步方法,会让原本多线程执行变成了单线程执行(异步变同步)
synchronized关键字
这个关键字可以修饰方法或代码块
public synchronized void sell(){
//该方法执行途中,其他线程无法插队
}
修饰代码块
修饰方法
写在方法的返回值前,这时该方法就称为同步方法
多线程相关面试题
实现多线程的方式
继承Thread类,创建对象
实现Runnable接口后,包装成Thread对象
匿名内部类
为什么说StringBuilder和ArrayList、HashMap是非线程安全
什么叫死锁?怎么产生?如何解决
模拟死锁出现的情况
定义两个线程类,线程A先获取资源A后,在获取资源B;线程B先获取资源B后,再获取资源A
如果对资源A和资源B使用synchronized进行同步
死锁的解决方法
方式一:让两个线程获取资源的顺序保持一致
如两个线程都先获取knife,再获取fork
方式二:在两个线程获取的资源A和B之前,再获取第三个资源C,对这个资源C使用synchronized