一切都是对象
Java的五个基本特性
万物皆对象
程序是对象的集合,它们通过发送消息来告知彼此想要做的事
每个对象都有自己的由其他对象所构成的存储
每个对象都拥有其类型
某一特定类型的所有对象都可以接收同样的消息
用引用操纵对象
Java中的数据类型分为:基本数据类型和引用数据类型
在Java中,一切都被视为对象,尽管一切都是对象,但是操纵的标识符其实一个对象的"引用"
对象存储的地方
寄存器
最快的存储区,位于处理器的内部,但是寄存器的数量极其有限,所以一般情况下,我们不能够直接控制只能根据需求进行分配
堆栈
位于RAM(随机访问存储器)中,通过堆栈指针可以从处理器那里获取直接支持,堆栈指针向上移动,则释放内存,若向下移动,则分配新的内存
堆
用于存放所有的Java对象,编译器不需要知道存储的数据在堆里存活多长时间,所以在堆里分配存储有很大的灵活性,但是用堆进行存储分配和清理可能比用堆栈进行存储分配需要更多的时间
常量存储
常量值通常直接存放在程序代码内部
非RAM存储
数据完全存活于程序之外,可以不受程序的任何控制,在程序没有运行时也可以存在,其中最基本的两个例子: 流对象和持久化对象
基本数据类型
byte short int long float double char boolean
默认值分别为:0 0 0 0 0.0 0.0 空字符 false
浮点数默认为double类型,要想表示float类型,需要加后缀f或F
浮点数还是存在一定的误差,所以如果在数值计算中不允许有误差,则可以使用BigDecimal类
整型值和布尔值之间不能进行相互转换
在Java中1和0并不能代替true和false
Java确定了每种基本类型所占的存储空间的大小,所以Java更具可移植性
BigInteger(整数)和BigDecimal(浮点数)必须以方法调用的方式取代运算符的方式实现
//使用静态的valueOf方法可以将普通的数值转换为大数值 //注:不能使用人们熟悉的+-*/来处理大数值,而需要使用大数值类的方法 add,subtract,multiply,divide,mod(取余) BigInteger c = a.add(b); //c = a+b BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))) //d = c*(b+2)
在java中不能再嵌套的两个块中声明同名的变量(在c++中可以,内层定义的变量会覆盖外层变量)
public static void main(String []args) { int n; ... { int n; //编译报错 } }
整数被0除将会出现一个异常,而浮点数将0除将会得到无穷大或NaN结果,不会报错
文档注释:JDK中包含了一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档
格式:/**
*/
控制执行流程及循环
/* 计算1000以内所有不能被7整除的整数之和 1. 解决一个问题的时候,可以先自己汉语理解,描述下思路 2. 复杂的问题可以一步一步去实现,没必要一步全部实现 */ int sum = 0; for(int i = 0;i <= 1000;i++){ if(i % 7 != 0){ sum += i; //sum = sum + i; } } System.out.println("sum: " + sum); /* 计算1 + 2 - 3 + 4 - 5...+100的结果 找规律: 奇数时减法,偶数时加法 第一种思路: 所有的偶数求和,所有的奇数求和,然后偶数求和的结果减去奇数求和的结果(1除外) 第二种思路: 循环过程中,取出每个值,判断该数是奇数还是偶数,偶数就加,奇数就减 */ int sum = 1; for(int i = 2;i <= 100;i++){ if(i % 2 == 0){ //偶数 sum += i; }else{ //奇数 sum -= i; } } System.out.println(sum); /* 从控制台输入一个正整数,计算该数的阶乘 */ Scanner scanner = new Scanner(System.in); System.out.println("请输入一个正整数: "); int val = scanner.nextInt(); long data = 1; //防止int溢出,必要时可以使用BigInteger for(int i = val;i > 1;i--){ data *= i; } System.out.println("计算结果为: " + data); /* 从控制台接受一个正整数,判断该数字是否是质数(质数是指在大于1的自然数中,除了1和它本身以外,不再有其他因数的自然数) */ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int data = scanner.nextInt(); boolean flag = true; for(int i = 2;i < data;i++){ if(data % i == 0){ flag = false; break; } } if(flag){ System.out.println(data + "是质数"); }else{ System.out.println(data + "不是质数"); } } /* 从控制台接收一个正整数,将该正整数作为行数,输出以下图形 例如: 输入5,输出 * *** ***** ******* ********* */ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个正整数,作为行数: "); int rows = scanner.nextInt(); for(int i = 1;i <= rows;i++){ //外层循环data次,控制的是总行数 //循环输出空格 for(int j = 0; j < rows - i;j++){ System.out.print(" "); } //循环输出"*" for(int k = 0; k < (2 * i - 1);k++){ System.out.print("*"); } System.out.println(); //换行 } } /* 小芳的妈妈每天给她2.5元,她都会存起来,但是,每当这一天是存钱的第5天或者是5的倍数的话,她都会花去6元,请问,多少天后,小芳可以存够100元 */ public static void main(String[] args) { int day = 1; double money = 2.5; while(money < 100){ //判断条件 day++; if(day % 5 == 0){ money -= 6; } money += 2.5; } System.out.println("小芳通过 " + day + " 天,存到了 " + money + " 元"); } /* 一个数如果恰好等于它的因子之和,这个数就是完数,请写一个程序找出1000以内所有的完数 */ public static void main(String[] args) { int count = 0; int sum; for(int i = 2; i <= 1000;i++){ //1不是完数,所以从2开始 sum = 0; //用来进行因子求和 for(int j = 1;j <= i / 2;j++){ //j只需要取前面的一半即可 if(i % j == 0){ sum += j; } } if(sum == i){ //是完数 count++; System.out.println("i = " + i); } } System.out.println("完数个数为:" + count); }
方法
方法定义
[修饰符列表] 返回值类型 方法名(形式参数列表){
方法体;
}
返回值:
返回值一般指的是一个方法执行结束之后的结果,该结果通常是一个数据,所以被称为'值'
返回值类型:
返回值类型可以是任何类型,只要是Java中的合法数据类型即可,数据类型包括基本数据类型和引用数据类型
方法就是为了完成某个特定的功能,方法结束之后大部分情况下都是有一个结果的,而体现结果的一般都是数据。
数据都有类型,就是返回值类型
当一个方法执行结束之后不返回任何值的时候,返回值类型也不能空白,必须写上void,void表示该方法执行结束之后不返回任何结果
JVM内存结构
方法执行时内存的变化
方法开始执行的时候,该方法所需要的内存空间在栈中进行分配,此时称为压栈操作
方法执行结束之后,该方法所需要的内存空间就会释放,此时称为弹栈操作
栈中存储的主要是方法在运行过程中需要的内存,以及存储方法的局部变量
三块内存中最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收主要针对的是堆内存
垃圾回收器【自动回收机制,GC机制】什么时候会考虑将某个java对象的内存回收?
当堆内存中的java对象成为垃圾数据时,会被垃圾回收器回收
什么时候堆内存中的java对象会变成垃圾?
没有更多的引用指向它的时候
这个对象无法被访问,因为访问对象只能通过引用的方式来进行访问
/* 编写一个方法,求整数n的阶乘 */ public static void main(String[] args) { System.out.println("请输入正整数n: "); Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); long res = func(n); //long类型,防止int类型溢出 System.out.println("res = " + res); } public static long func(int n){ long result = 1; for(int i = 1;i <= n;i++){ result *= i; } return result; }
/*
编写一个方法,输出大于某个正整数n的最小的质数
/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个正整数n: ");
int n = scanner.nextInt();
int res = n;
/
while(!zhishu(++n)){ //简化版写法
}
System.out.println("大于正整数 " + n + " 的最小的质数为: " + res);
*/
while(true){
res++;
if(zhishu(res)){ //判断n是否是质数
break;
}
}
System.out.println("大于正整数 " + n + " 的最小的质数为: " + res);
}
public static boolean zhishu(int n){
for(int i = 2;i < n;i++){
if(n % i == 0){
return false; //不是质数
}
}
return true; //是质数
}
4. 方法重载 1. 优点: 1. 代码整齐美观 2. "功能相似"的,可以让方法名相同,更易于代码编写 2. 编译器是如何进行方法区分的呢? 1. 首先,Java编译器会通过方法名进行区分,但是在Java语言中是允许方法名相同的情况出现的 2. 在方法名相同的情况下,编译器会通过方法的参数类型进行方法的区分 3. 什么时候可以考虑使用方法重载机制? 1. 在同一个类中,如果"功能1"和“功能2”相似,那么就可以考虑将它们的方法名一致,这样代码既美观,又便于后期代码编写 2. 注意: 方法重载机制不能随便使用,如果两个功能压根不相干,就不可以使用重载 4. 什么时候编写代码会发送方法重载? 1. 在同一个类中 2. 方法名相同 3. 参数列表不同 1. 参数的个数不同 2. 参数的类型不同 3. 参数的顺序不同 5. 方法重载和方法的"返回值类型"无关 6. 方法重载和方法的"修饰符列表"也无关 5. 方法递归 1. 递归就是方法自己调用自己 2. 递归的时候,若程序没有退出条件,一定会发生栈内存溢出错误(StackOverFlowError),所以递归的时候一定要有结束条件 3. 在实际的开发中,不建议轻易的使用递归,能用循环解决的,尽量使用循环来做,因为循环的效率高,耗费的内存小,递归耗费的内存比较大,而且如果递归使用的不恰当,会导致JVM挂掉 (但是在极少数的情况下,必须要用到递归才能实现某个功能) 4. 假设有一天真的遇到了"栈内存溢出错误",我们该怎么解决,具体思路是什么? 1. 首先,我们可以查看递归结束的条件是否正确,如果不对,必须对条件做进一步的修改,直到正确为止 2. 如果递归条件没有任何问题,那么这个时候,我们就需要手动的调整JVM的栈内存初始化大小,可以将其调的大一点 3. 如果调整了大小之后还是出现错误,那么我们只能继续扩大栈的内存大小 4. 通过java -X可以查看调整堆栈大小的参数 ```java /* 使用递归,计算1-n的和 */ public static void main(String[] args) { System.out.println("res = " + sum(10)); } public static int sum(int n){ if(n == 1){ return 1; //递归结束条件 } return n + sum(n - 1); } /* 使用递归,计算n的阶乘 */ public static void main(String[] args) { System.out.println("res = " + func(5)); } public static int func(int n){ if(n == 1){ return 1; } return n * func(n - 1); } ``` ## 常用类 ### String 1. String类的substring方法可以从一个较大的字符串提取出一个字串substring(a,b); 截取字串的长度为b-a,包括a,不包括b 2. Java允许使用+进行两个字符串的拼接 3. String字符串的存储原理 1. **Java中的字符串都是直接存储在方法区中的"字符串常量池"当中** 2. 因为字符串在实际开发中使用过于频繁,所以为了执行效率较高,于是创建了"字符串常量池" 3. 注意:垃圾回收器是不会释放常量池中的常量的 ```java //String类重写了equals(Object object)方法,所以字符串对象之间的比较不能使用"==",而应该使用equals方法 String s1 = "hello"; String s2 = "hello"; System.out.println(s1 == s2); //true String s3 = new String("xy"); String s4 = new String("xy"); System.out.println(s3 == s4); //false System.out.println(s3.equals(s4)); //true //比较对象 String a = "qwer"; //a = null; System.out.println("qwer".equals(a)); //字符串比较时,建议使用这种方式,可以避免空指针异常 System.out.println(a.equals("qwer")); //存在空指针异常的风险,所以不建议这样写
当将一个字符串与一个非字符串的值进行拼接时,后者将会被转换为字符串(任何一个java对象都可以转换为字符串)
如果需要把多个字符串放在一起,用一个界定符分隔,可以使用静态方法join
String s1 = String.join(".","c","h","e","n"); System.out.println(s1);
java中字符串是不能改变的,可以使用equals()方法检测两个字符串是否相等(该方法区分大小写)(尽量不要使用==来检测两个字符串是否相等)
要想检测两个字符串是否相等,而不区分大小写,则可以使用equalsIgnoreCase方法
String中charAt(n)函数返回位置为n的代码单元,n介于0到字符串长度-1
//构造方法 String s1 = "hello world"; //创建字符串对象最常用的一种方式 System.out.println(s1); //hello world byte[] bytes = {97,98,99}; //97是a,98是b,99是c String s2 = new String(bytes); System.out.println(s2); //abc //String(字节数组,数组元素下标的起始位置,长度) 将btye数组中的一部分转化为字符串 String s3 = new String(bytes,1,2); System.out.println(s3); //bc //将char数组全部转换为字符串 char[] chars = {'我','永','远','爱','昕','昕'}; String s4 = new String(chars); System.out.println(s4); //将char数组中的一部分转化为字符串 String s5 = new String(chars,4,2); System.out.println(s5);
//按照字典顺序,如果字符串位于s之前,返回一个负数,如果位于s之后,返回一个正数,如果相等返回0
int res1 = "abc".compareTo("abc");
System.out.println(res1); //0 前后一致
int res2 = "abcd".compareTo("abce");
System.out.println(res2); //-1 前小后大
int res3 = "abce".compareTo("abcd");
System.out.println(res3); //1 前大后小
//返回指定索引的char类型的值
String s1 = "我永远爱昕昕";
char c1 = s1.charAt(3);
System.out.println(c1);
//将字符串对象转换为字节数组
byte[] bytes = "hello xinxin".getBytes();
for(int i = 0;i < bytes.length;i++){
System.out.print(bytes[i] + " ");
}
//如果x<y,则返回一个负整数,如果x和y相等,则返回0,否则返回一个正整数
static int compare(int x,int y)
//如果字符串以s开头,返回true
boolean startsWith(String s)
//判断两个字符串是否相等,同时忽略大小写
System.out.println("Abc".equalsIgnoreCase("aBC")); //true
//如果字符串以s结尾,返回false
System.out.println("test.txt".endsWith(".java")); //false
System.out.println("test.txt".endsWith(".txt")); //true
//返回与字符串str匹配的最后一个子串的位置
int lastIndexOf(String str)
//返回与字符串str匹配的第一个子串的位置,如果不存在则返回-1
int indexOf(String str)
//返回一个新的字符串,这个字符串用newChar代替所有的oldChar
s.replace(oldChar, newChar)
//将字符串转换为char数组
char[] chars = "我爱昕昕".toCharArray();
for(int i = 0;i < chars.length;i++){
System.out.print(chars[i] + " ");
}
//返回一个新的字符串,这个字符串将原始字符串中的大写改为小写,小写改为大写
String toLowerCase()
String toUpperCase()
//返回一个新的字符串,这个字符串将删除原始字符串头部和尾部的空格
String trim()
//读取输入的下一行内容
String nextLine()
//读取输入的下一个单词
String next()
//检测输入中是否还有其它单词
boolean hasNext()
//检测输入中是否还有其他整数
boolean hasNextInt()
//如何输入一个字符
Scanner s = new Scanner(System.in);
String s1 = s.next();
char c = s1.charAt(0);
//判断是否包含某个字符串
System.out.println("hello world".contains("worl")); //true
//判断字符串是否为空,以及空的两种状态:
String s2 = ""; //表示空字符串
String s3 = null; //表示空对象,空指针异常
System.out.println(s2.isEmpty()); //true
//字符串分割
s.split(String str)
//将"非字符串"转换为"字符串"
String s3 = String.valueOf(true);
System.out.println(s3); //此时的true不是boolean类型的,而是String类型的
9. String字符串的构造(一旦被赋值便不能被修改) 10. StringBuffer和StringBuilder可以创建一个可以被修改的字符串,使用起来基本相同 不同:StringBuffer线程安全,但是性能略低,StringBuilder线程不安全,但是性能略高 11. StringBuffer和StringBuilder 1. StringBuffer和StringBuilder底层实际上都是一个char[]数组 2. 往StringBuffer和StringBuilder中放字符串,实际上是放入到了char数组中 3. StringBuffer和StringBuilder的初始化容量为16,如果容量不够,底层便会使用数组自动扩容来增大容量 那如何优化StringBuffer或者StringBuilder的性能呢? 1. 在创建StringBuffer和StringBuilder的时候,尽可能给定一个初始化容量,最好较少底层数组的扩容次数,预估计一下,给一个大一些的初始化容量 4. 关键点:给一个合适的初始化容量,可以提高程序的执行效率 12. 因为String中的char数组被final修饰了,所以不可变,而StringBuffer和StringBuilder中的char数组并没有被final修饰,所以可变 13. 区别 1. StringBuffer中的方法都有synchronized关键字修饰,表示StringBuffer在多线程环境下运行是安全的 2. StringBuilder中的方法没有synchronized关键字修饰,表示多线程环境下运行不安全 3. 如果考虑线程安全,就使用StringBuffer,如果考虑执行效率,不考虑安全,就可以使用StringBuilder ```java //源代码 public StringBuffer() { super(16); } AbstractStringBuilder(int capacity) { value = new char[capacity]; } ```
包装类
Java中为8种基本数据类型又对应准备了8种包装类,属于引用数据类型,继承自Object
八种包装类分别是:Byte、Short、Integer、Long、Float、Double、Boolean、Character
装箱和拆箱
- 装箱:基本数据类型---->引用数据类型
- 拆箱:引用数据类型---->基本数据类型
通过访问Integer包装类的常量,来获取最大值和最小值
System.out.println("Integer最大值: " + Integer.MAX_VALUE); System.out.println("Integer最小值: " + Integer.MIN_VALUE); //输出 Integer最大值: 2147483647 Integer最小值: -2147483648
/*
- "=="并不会触发自动装箱和自动拆箱
- 注意: Java中为了提高程序的执行效率,将-128到127之间所有的包装对象提前创建好,放到了一个方法区的"整数型常量池"当中,目的是只要用这个区间的数据
就不需要再从堆空间中去new对象了,直接从常量池中取出即可(Integer类加载的时候,会初始化整数形常量池,生成256个对象)
*/
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); //true 因为i1变量中保存的内存地址和i2变量中保存的内存地址是一样的
Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false
//字符串转int
int i = Integer.parseInt("123");
5. JDK 1.5之后,就支持自动装箱和自动拆箱(即基本数据类型和八种引用数据类型可以自动转换) ### 日期相关类 ```java //获取当前时间(精确到毫秒的系统当前时间) Date date = new Date(); System.out.println(date); //Wed Dec 30 22:15:46 CST 2020 /* 日期是否可以格式化呢,将上面的日期按照指定的格式进行转换 SimpleDateFormat是java.text下专门负责日期格式化的类 yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒 SSS 毫秒 注意:在日期格式中,除了y M d H m s S这些字符不能随便写之外,其他的符号自己可以随意组织 */ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); String format = sdf.format(date); System.out.println(format); //2020-12-30 22:26:28 446 //假设现在有一个日期字符串,怎么转换成日期类型呢 String time = "2020-12-31 08:08:30 666"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); //格式不能随便写,要和日期字符串格式相同 Date date = sdf.parse(time); System.out.println(date); //获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数 long nowTimeMills = System.currentTimeMillis(); System.out.println(nowTimeMills); //通过毫秒构造Date对象 Date date = new Date(2); //注意:参数是一个毫秒数 1970-01-01 08:00:00:002 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); String s = simpleDateFormat.format(date); System.out.println(s);
Math类
1.PI E
2.三角函数相关方法
3.round(x) 四舍五入
4.sqrt() 平方根
5.pow(x,y) x的y次方
6.abs() 绝对值
7.ceil() 向上取整
8.floor() 向下取整
9.max(x,y) 最大值
10.min(x,y) 最小值
Random
//随机数的产生 Random random = new Random(); int num1 = random.nextInt(); //随机产生一个int类型取值范围内的数字 int num2 = random.nextInt(10); //产生0~10之间的随机数,不包括10 //编写程序,生成5个0-100内不重复的随机数,重复的话重新生成,将最终生成的5个随机数放到数组中 public static void main(String[] args) { int[] arr = new int[5]; Random random = new Random(); //为避免和0冲突,最好先提前赋值 for(int i = 0;i < arr.length;i++){ arr[i] = -1; } int index = 0; while (index < arr.length){ //只要数组没有填满,就一直生成随机数 int num = random.nextInt(101); //随机生成0-100的数 if(!isExitNum(arr,num)){ arr[index++] = num; } } for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + " "); } } //判断数组中是否包含某个元素 public static boolean isExitNum(int[] arr,int num){ for(int i = 0; i < arr.length;i++){ if(arr[i] == num){ return true; } } return false; }
类与对象及访问权限控制
面向对象三大特性
- 封装
- 继承
- 多态
- 类:实际上在现实世界中是不存在的,是一个抽象的概念,是一个模板
- 对象:是实际存在的个体,要想得到对象,必须先定义类,对象是通过类这个模板创造出来的
- 类 = 属性 + 方法
- 属性:状态
- 方法:动作
- 创建对象:类名 变量名 = new 类名();
- 重点:凡是通过new运算符创建的对象,都存储在堆内存当中,new运算符的作用就是在堆内存当中开辟一块空间 (局部变量和引用存储在栈内存当中)
- Java中不管是赋值还是传参,都是值传递
- 空指针异常出现的条件:当任何一个空引用访问实例(对象)相关的数据时,都会出现空指针异常
- 构造方法
- 创建对象,并且在创建对象的同时给属性进行赋值(初始化)
- 当一个类中没有提供任何构造方法时,系统会默认提供一个无参构造方法
- 当一个类中手动提供了构造方法,那么系统将不再提供默认的无参构造,若这个时候想使用无参构造方法,则必须自己编写
- 无参构造和有参构造都是可以调用的
- 构造方法支持重载,在一个类中构造方法可以有多个,而且名字都是一样的,只是参数列表不同
封装
作用
- 保证内部结构的安全
- 屏蔽复杂,暴露简单
在代码级别上,封装有什么作用?
- 在一个类体当中的数据,假设封装之后对于代码的调用人员来说,不需要关心内部代码的复杂实现,只需要通过一个简单的入口就可以访问了。另外,类体中安全级别较高的的数据封装起来,外部人员不能随意访问,保证了数据的安全性。
对外提供公开的set和get方法作为操作入口,并且不带static,访问私有属性
注意
Java开发规范中有要求,set和get方法要满足以下格式
get方法的要求:
public 返回值类型 get + 属性名首字母大写(无参){
return 属性名;
}
set方法的要求:
public void set + 属性名首字母大写(有一个参数){
属性名 = 参数;
}
作为程序员,应该尽量按照Java规范中的要求格式提供set和get方法,如果不按照这个规范格式写,那么
我们写的程序将不是一个通用的程序
认识static
静态的,所有static关键字修饰的全是类相关的,采用"类名."的方式访问
实例变量:对象相关的,访问时采用"引用."的方式访问,需要先new对象再访问
静态变量:类相关的,访问时建议采用"类名."的方式访问,也可以使用"引用."的方式访问(一般不建议使用),不需要new对象
静态变量在类加载的时候进行初始化,存储在方法区
什么时候将变量声明为实例的,什么时候声明为静态的?
如果这个类型的所有对象的某个属性都是一样的,不建议定义为实例变量,浪费内存空间,建议定义为静态变量,在方法区中只保留一份,节省内存开销
只有在"空引用"访问实例相关的,都会出现空指针异常,访问静态相关的,不会出现空指针异常
静态代码块
static{
Java语句;
}
- static静态代码块是在类加载的时候执行的,而且在main方法执行之前执行,并且只执行一次
- 静态代码块一般是按照自上而下的顺序执行
- 静态代码块的作用
- 在类加载的时候做一些初始化的准备工作
- 不太常用,根据具体的业务需求来定
实例代码块
{
Java语句;
}
只要是构造方法执行,必然在构造方法执行之前,自动执行实例代码块中的代码
this
this是一个引用,保存了当前对象的内存地址,指向当前对象自身,严格意义上来说,this代表的就是"当前对象",每个对象都有一份
this存储在堆内存当前对象的内部
this只能使用在实例方法当中,谁调用这个实例方法,this就是谁
this除了可以使用在实例方法中,还可以用在构造方法中
语法:通过当前的构造方法去调用当前类的另外一个构造方法,做到代码复用
this(实际参数列表)
注意:对于this(实际参数列表)的调用只能出现在构造方法的第一行,而且只能出现一次
访问权限修饰符:
public protected default private
同一个类 √ √ √ √
同一个包 √ √ √ ×
子父类 √ √ × ×
不同包 √ × × ×
继承
作用
- 基本作用:子类继承父类,代码可以得到复用
- 主要作用:因为有了继承关系,才会有后面的方法覆盖和多态机制
相关特性
B类继承A类,则称A类为超类(superclass)、父类、基类,B类称为子类(subclass),派生类、扩展类
class B extends A{
Java语句:
}
Java中的继承只支持单继承,c++支持多继承
虽然Java不支持多继承,但是有时候会产生间接继承的效果,例如:class c extends B,class B extends A,也就是说,c直接继承B,间接继承A
Java中规定,子类继承父类,除了构造方法不能被继承外,其余的都可以继承,但是私有化的属性和方法在子类当中是无法访问的
Java中的类如果没有显示的继承任何类,那么默认继承Object类,Object类是所有类的根类
继承的缺点,会导致子类和父类之间的耦合度非常高,一旦父类被修改,子类就会受牵连
方法覆盖
- 子类继承父类之后,当继承过来的方法无法满足当前子类的业务需求时,子类有权利对这个方法进行重新编写,有必要进行方法的覆盖
- 方法覆盖又叫方法重写(override)
- 结论:当子类对父类继承过来的方法进行方法覆盖之后,子类调用该方法的时候,就会执行覆盖之后的方法
- 代码怎么编写,才会构成方法覆盖
- 两个类必须要继承关系
- 重写之后的方法和之前的方法必须具有:
- 相同的返回值类型
- 相同的方法名
- 相同的方法参数列表
- 访问权限不能更低,可以更高
- 重写之后的方法不能比之前的方法抛出的异常更多,可以更少
- 几个注意事项
- 方法覆盖只是针对于方法,和属性无关
- 私有方法无法覆盖
- 构造方法不能被继承,所以也无法被覆盖
- 方法覆盖只是针对于"实例方法","静态方法"覆盖没有意义
多态
学习多态语法之前,需要知道两个概念
向上转型(相当于c++中的自动类型转换)
子类------------>父类
向下转型(相当于c++中的强制类型转换)
父类------------>子类
注意:Java中支持向上转型,也支持向下转型,但是两种类型之间必须有继承关系,如果没有继承关系,编译器就会报错
多态就是父类引用指向子类对象,其中包括编译阶段和运行阶段(编译阶段静态绑定父类的方法,运行阶段动态绑定子类的方法)
可以将子类对象赋给父类引用,但是不能将一个父类的对象赋给子类引用
什么时候必须使用向下转型?
一般情况下,不要随便做类型转换,当你需要访问的是子类对象中"特有"的方法,这个时候就必须进行向下转型
向下转型的风险
instanceof运算符:可以在运行阶段动态判断引用指向的对象的类型
语法:(引用 instanceof 类型)
如果要进行向下转型,将父类转换为子类,应该用instanceof进行检查
运算结果只能是true/false
假设a是一个引用,a中保存了内存地址指向的堆中的对象
如果a instanceof 类型----->true 表示a引用指向的堆内存当中对象是该类型的一个对象,此时是可以进行向下转型的
如果a instanceof 类型----->false 表示a引用指向的堆内存当中对象并不是该类型的一个对象,此时就不能进行向下转型
Java规范中要求:任何时候进行向下转型的时候,一定要使用instanceof运算符进行判断,这样可以很好的避免"ClassCastException"类型转换异常
如果x = null x instanceof C 不会产生异常,只是返回false,之所以这样是因为null没有引用任何对象,当然也不会引用C类型的对象
学习多态机制后,方法覆盖时说的"相同的返回值类型"可以修改吗?
- 对于返回值类型为基本数据类型来说,必须一致
- 对于返回值类型为引用数据类型来说,重写方法之后的返回值类型可以更小(但意义不大,一般不常用)
super关键字
- super能出现在实例方法和构造方法中,但是和this一样,不能出现在静态方法中
- super不是引用,也没有保存某一个对象所分配内存空间的地址,所以无法直接输出super,但是可以直接输出this
- super的语法:super. super()
- “super.”大部分情况下是可以省略的,但是当父类和子类中有同名属性,或者有同名的方法时,想要在子类中访问父类的属性或方法,必须使用"super."
- 具体使用
- super.属性名 访问父类的属性
- super.方法名 访问父类的方法
- super(实参) 调用父类的构造方法
- super()和this()一样,只能出现在构造方法的第一行,通过当前的构造方法去调用"父类"中的构造方法,目的是:创建子类对象的时候,先初始化父类的属性
- 重要结论:当一个构造方法第一行既没有this(),又没有super()的时候,编译器默认会有一个super(),表示通过当前子类的构造方法调用父类的无参构造方法,所以正常情况下必须保证父类的无参构造方法是存在的
- super()和this()不能共存,它们都只能放在构造方法的第一行
子类覆盖父类的方法时,子类方法不能低于父类方法的可见性
在java中,子类数组的引用可以转换为父类数组的引用,而不需要采取强制类型转换(两个引用必须引用的是同一个数组)
/* 编写Java程序模拟简单的计算器 定义名为Number的类,其中有两个整型变量n1,n2定义为私有属性 编写构造方法对属性进行赋值 再为该类添加加addition(),减subtration(),乘multiplication(),除division()等公有实例方法 分别对两个成员变量执行加、减、乘、除的运算 在main方法中测试 */ public class Test01 { public static void main(String[] args) { Number n = new Number(10,2); n.additon(); n.subtration(); n.multiplication(); n.division(); } } class Number{ private int n1; private int n2; public Number() { } public Number(int n1, int n2) { this.n1 = n1; this.n2 = n2; } public int getN1() { return n1; } public void setN1(int n1) { this.n1 = n1; } public int getN2() { return n2; } public void setN2(int n2) { this.n2 = n2; } //加 public void additon(){ int res = n1 + n2; System.out.println(n1 + " + " + n2 + " = " + res); } //减 public void subtration(){ int res = n1 - n2; System.out.println(n1 + " - " + n2 + " = " + res); } //乘 public void multiplication(){ int res = n1 * n2; System.out.println(n1 + " * " + n2 + " = " + res); } //除 public void division(){ if(n2 == 0){ System.out.println("除数不能为0..."); return; } int res = n1 / n2; System.out.println(n1 + " / " + n2 + " = " + res); } @Override public String toString() { return "Number{" + "n1=" + n1 + ", n2=" + n2 + '}'; } }
final关键字
在Java中,利用关键字final指示常量(关键字final表示这个变量只能被赋值一次,一旦被赋值,便不能够在更改,一般常量名全部使用大写)
如果希望阻止人们利用某个类定义子类,则可以将这个类定义为final类
public final class
final类中的所有方法都是final方法,final类的引用,它引用的一定是本类的对象,不可能是其他类的对象
final修饰的类无法被继承
final修饰的方法无法被覆盖
final修饰的变量,必须手动赋值,系统不负责赋默认值,这个手动赋值,可以在变量后面赋值,也可以在构造方法中赋值
final修饰的引用,一旦指向某个对象之后,便不能再指向其他的对象,被指向的对象无法被垃圾回收器回收
final修饰的引用虽然指向某个对象之后不能再指向其他对象,但是所指的对象内部的内存是可以被修改的
final修饰的实例变量是不可变的,这种变量一般和static联合使用,被称为"常量"
语法格式:
public static final 数据类型 常量名 = 值
抽象类
抽象类无法实例化,无法创建对象,为什么?
抽象类是类与类之间的共同特征,将这些具有共同特征的类再进一步抽象形成了抽象类,由于类本身是不存在的,所以抽象类是无法创建对象的,一般 抽象类是用来被子类继承的
抽象类也属于引用数据类型,语法:
[修饰符列表] abstract class 类名{
类体;
}
final和abstract不能联合使用,这两个关键字是对立的
抽象类的子类也可以是抽象类
抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类调用的
抽象方法:
抽象方法表示没有实现的方法,没有方法体的方法
public abstract void test();
特点:
- 没有方法体,以分号";"结尾
- 前面修饰符列表中有abstract关键字
抽象类中不一定有抽象方法,也可以有其他的非抽象方法,但是抽象方法必须出现在抽象类中
重要结论:Java语法强制规定,一个非抽象类继承抽象类,必须将抽象类中的所有抽象方法全部实现(覆盖或者重写),否则编译器报错。如果不想将抽象方法实现,那么就必须将实现类也定义为抽象类
面向抽象编程:抽象类(父类)引用指向子类对象
接口
接口也是一种"引用数据类型",编译之后也是一个class字节码文件
抽象类是半抽象的,但是接口是完全抽象的,也可以说接口是特殊的抽象类
定义
[修饰符列表] interface 接口名{
接口中全是抽象方法,所以是没有方法体的
}
接口支持多继承,一个接口可以同时继承多个接口
接口中只包含两部分内容:一部分是常量,一部分是抽象方法
接口中的所有属性和方法都是public修饰的,都是公开的,所以在接口中定义抽象方法(常量)时,public abstract(public static final)修饰符都可以省略
当一个非抽象类实现接口的话,必须将接口中所有的抽象方法全部实现
当继承和实现都存在的话,extends写在前面,implements写在后面
接口在开发中的作用:类似于多态在开发中的作用
- 多态在开发中的作用:面向抽象编程,不要面向具体编程,降低了程序的耦合度,提高了程序的扩展力
Object类中的几个方法
public String toString() 返回对象的字符串表示形式
public boolean equals(Object obj) 判断两个对象是否相等
判断两个基本数据类型的数据是否相等,直接使用"=="就行,但是判断两个对象是否相等,就只能使用equals方法
字符串里面的equals比较的是里面的值,因为字符串重写了equals方法,引用类型的equals比较的是还是引用
在Object类中的equals方法,默认采用的是 "==" 来判断两个对象是否相等,而这个判断的是两个对象的内存地址是否相等,我们应该判断两个对象里面的内容是否相等,所以我们需要重写Object类的equals方法
//Object类中的equals方法 public boolean equals(Object obj) { return (this == obj); }
4. equals方法的特性: 1.自反性 :对于任何非空引用x,x.equals(x)应该返回true 2.对称性 :对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true 3.传递性 :对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true 4.一致性 :如果x和y引用的对象没有发生变化,则反复调用x.equals(y)应该返回同样的结果 5.对于任何非空引用x,x.equals(null)应该返回false 3. protected void finalize() GC垃圾回收器自动调用的方法 4. clone() 能够复制一个对象,生成一个新的引用,分配新的内存空间 1. 一个类必须实现Cloneable接口,才能被克隆 2. clone方法相当于c+++中的浅拷贝 3. 浅克隆:Object类中的clone是浅克隆,就是基本数据类型属性完全重新创建,引用类型属性依然共用的克隆 4. 深克隆:将所有类型(包括基本数据类型和引用数据类型)属性都完全重新创建 (要想实现深克隆,必须自行重写clone方法) 5. 克隆是用来创建对象的一种方式,当对象数据量比较大,变化又相对较少时,使用克隆能够一定程度上减少产生对象的开销
内部类
定义:在类的内部又定义了一个新的类,称为内部类
分类
静态内部类:类似于静态变量
- 如果只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外部类对象,因此可以将内部类声明为static
- 只有内部类才是可以声明为static的,静态内部类可以有静态域和静态方法
实例内部类:类似于实例变量
- 常用的是匿名内部类:适用于只需要使用一次的类,通过接口或者抽象类创建内部类
局部内部类:类似于局部变量
不能用public或private访问说明符进行说明,它的作用域被限定在声明这个局部类的块中
它的优势:
即对外部世界可以完全地隐藏起来
不仅能够访问包含它们的外部类,而且还可以访问局部变量,但是那些局部变量必须为final
内部类可以访问该类定义所在的作用域中的数据,包括私有数据
内部类可以对同一个包中的其他类隐藏起来
当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷
内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象,通过这个引用可以访问外围类对象的全部状态(static内部类没有该隐式引用)
非静态内部类里面是不能定义静态方法的,可以有静态属性,但是内部类里面的静态属性都必须是final
回调函数:你写一个函数,把函数地址赋值一个函数指针,然后把这个函数指针当作参数赋给另一个函数,另一个函数通过函数指针的地址调用这个函数
外部类访问权限:public default
内部类访问权限:public default protected private static
外部类不能直接访问内部类的成员,可以通过对象访问,内部类可以访问外部类的所有成员,包括私有成员
因为外部类对象跟内部类对象是一对多的关系,外部类对象可以包含多个内部类对象,不知道访问哪一个
(内部类对象可有可无,只有有外部类对象的时候,内部类对象才有意义)
使用内部类编写的代码,可读性很差,能不用尽量不用
异常处理
什么是异常?Java提供异常机制的作用?
- 程序执行过程中发生了不正常的情况,叫做异常
- Java是一门很完善的语言,提供了异常的处理方式,Java把异常信息打印输出到控制台,我们看到异常信息后,便可以对程序进行修改,让程序更加健壮
异常在Java中是以类的形式存在的,每一个异常类都可以创建异常对象
//创建异常对象 NullPointerException exception = new NullPointerException("空指针异常..."); System.out.println(exception); //java.lang.NullPointerException: 空指针异常... //实际上程序运行到这的时候,会new一个异常对象:new ArithmeticException("/ by zero"),然后由JVM将new的异常对象抛出,打印到控制台 int val = 1 / 0;
异常的继承机构
编译时异常和运行时异常的区别
- 编译时异常和运行时异常都是发生在运行阶段的,编译阶段是不会发生异常的,因为只有在运行阶段才可以new对象
- 编译时异常:表示异常必须在编译阶段时进行预处理,如果不处理编译器就会报错
- 一般编译时异常发生的概率较高,运行时异常发生的概率较低
异常的两种处理方式
- 在方法声明的位置上,使用throws关键字将异常抛出,抛给上一级进行处理,上一级可以继续选择抛出或者捕捉
- 使用try...catch语句进行异常的捕捉
throws和try...catch联合使用
//源码 //从这里可以看出,只要调用FileInputStream(String name),调用方要么选择throws抛出,要么进行try...catch捕捉 public FileInputStream(String name) throws FileNotFoundException { this(name != null ? new File(name) : null); }
public static void main(String[] args) {
System.out.println("main begin...");
try {
m1();
System.out.println("hello xinxin");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("main end...");
}
public static void m1() throws FileNotFoundException {
System.out.println("m1 begin...");
m2();
System.out.println("m1 end...");
}
public static void m2() throws FileNotFoundException {
System.out.println("m2 begin...");
m3();
System.out.println("m2 end...");
}
public static void m3() throws FileNotFoundException {
new FileInputStream("F:\学习笔记\JavaSE\JavaSE.md"); //这里使用FileInputStream只是为了演示异常的抛出和捕捉
}
//未出现异常时的输出
main begin...
m1 begin...
m2 begin...
m2 end...
m1 end...
main end...
//出现异常时的输出
main begin...
m1 begin...
m2 begin...
文件不存在...
main end...
7. 只要异常没有被捕捉,采用抛出的方式,那么抛出异常的那一行的后续代码便不会执行,另外,try语句块中的某一行出现异常时,该行后面的代码不会执行,try...catch语句捕捉异常之后,后续代码可以继续执行 8. 深入try...catch 1. catch后面的小括号中的类型可以是具体的类型,也可以是该异常类型的父类型 2. catch可以写多个,建议写catch的时候,精确的一个一个处理,这样有利于程序的调试 3. catch写多个的时候,从上到下,必须遵守类型从小到大 9. 异常对象的两个非常重要的常用方法 1. 获取异常简单的描述信息 e.getMessage() 2. 打印异常追踪的堆栈信息,实际开发中,建议使用这个打印方法 e.printStackTrace() ```java NullPointerException exception = new NullPointerException("空指针异常..."); String message = exception.getMessage(); System.out.println(message); exception.printStackTrace(); //打印异常堆栈信息(Java后台打印异常堆栈信息的时候,采用的是异步线程的方式进行打印的) System.out.println("hello world"); //输出 空指针异常... hello world java.lang.NullPointerException: 空指针异常... at com.siki.exception.ExceptionTest03.main(ExceptionTest03.java:6) ``` 10. finally语句 1. 不管是否有异常被捕获,finally子句中的都将会执行 2. try语句可以只有finally子句,而没有catch子句 3. finally语句块中通常进行资源的释放/关闭 4. 如果是退出了JVM之后,finally语句中的代码就不会执行了 5. 如果一个方法的try语句块中有return语句,而且finally子句中也有return语句,那么在try语句块执行到return语句时,便会执行finally子句,那么finally子句中的return就会覆盖try语句块中的return语句 ```java /* 关于finally的一道面试题 Java中的两条语法规则 1. 方法体中的代码必须遵循自上而下依次执行 2. return语句一旦执行,整个方法必须结束 以下代码的执行顺序: int i = 1; int j = i; i++; return j; //return语句一定是最后执行的,它前面还是会执行finally语句,只不过返回的是原先的值 */ public static void main(String[] args) { int res = func(); System.out.println(res); //输出:1 } public static int func(){ int i = 1; try{ return i; }finally { i++; } } ``` 11. 自定义异常类 1. 编写一个类继承Exception(编译时异常)或者RuntimeException(运行时异常) 2. 提供两个构造方法:一个无参构造、一个带有String参数的有参构造 ```java //自定义异常类 public class MyException extends Exception{ public MyException(){ } public MyException(String s){ super(s); } } public static void main(String[] args) { MyException exception = new MyException("用户名不能为空..."); String message = exception.getMessage(); //获取异常简单描述信息 System.out.println(message); //打印异常堆栈信息 exception.printStackTrace(); } //输出 用户名不能为空... com.siki.exception.MyException: 用户名不能为空... at com.siki.exception.MyExceptionTest.main(MyExceptionTest.java:6) ``` 12. 异常与方法覆盖 1. 如果父类的方法没有抛出任何编译异常,那么子类也不能抛出任何编译异常 2. 如果在子类中覆盖了一个父类的方法,子类方法中声明的编译异常不能比父类方法中声明的异常更通用 (也就是说,子类可以抛出更特定的异常,或者根本不抛出任何异常) ```java /* 1. 编写程序模拟用户注册 1. 程序开始执行时,提示用户输入"用户名"和"密码" 2. 输入用户信息后,后台Java程序开始模拟用户注册 3. 注册时要求用户名长度为[6-14]之间,小于或者大于都表示异常 */ public static void main(String[] args) { //测试 UserService service = new UserService(); try { //注册 service.register("admin", "123456"); } catch (IllegalNameException e) { //e.printStackTrace(); System.out.println(e.getMessage()); } } //用户业务类,处理用户相关的注册、登录等相关功能 class UserService{ public void register(String username,String password) throws IllegalNameException { //引用==null这个判断最好放在所有条件的最前面,而且建议写成null==引用,这样可以避免空指针异常,让程序更加健壮 if(null == username || username.length() < 6 || username.length() > 14){ throw new IllegalNameException("用户名不合法,长度必须在[6-14]之间"); //将异常抛出 } //程序如果执行到这,说明用户名合法 System.out.println("注册成功,欢迎[" + username + "]"); } } //自定义异常 class IllegalNameException extends Exception{ public IllegalNameException(){ } public IllegalNameException(String s){ super(s); } } ``` ## 集合 ### 集合概述 1. 集合实际上就是一个容器,可以容纳其他类型的数据,数组实际上就是一个集合 2. 注意:集合不能直接存储基本数据类型,也不能直接存储java对象,集合中存储的都是java对象的内存地址(或者说集合当中存储的是引用) 3. 在Java中每一个不同的集合,底层会对应不同的数据结构,往不同的集合中存储元素,等于将数据放到了不同的数据结构当中 4. 集合的继承结构图 ![image-20210103220411197](https://uploadfiles.nowcoder.com/images/20190919/56_1568900435177_29C080A5413E925FE3B3CCB4048AB99B) 5. Map集合继承结构图 ![image-20210103221543918](https://uploadfiles.nowcoder.com/images/20190919/56_1568900435177_29C080A5413E925FE3B3CCB4048AB99B) 6. 总结 1. ArrayList:底层是数组 2. LinkedList:底层是双向链表 3. Vector:底层是数组,线程安全的,效率较低,使用较少 4. HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合中的key部分 5. TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合中的key部分 6. HashMap:底层是哈希表 7. Hashtable:底层也是哈希表,只不过是线程安全的,效率较低,目前使用较少 8. Properties:底层是哈希表,是线程安全的,并且key和value只能存储String字符串 9. TreeMap:底层是二叉树,TreeMap集合中的key可以按照大小顺序进行排序 7. List集合存储元素的特点 1. 有序可重复 2. 有序:指存进去的顺序和取出来的顺序相同,每一个元素都有下标 3. 可重复:指可以存相同的元素 8. Set集合存储元素的特点 1. 无序不可重复 2. 无序:指存进去的顺序和取出来的顺序不一定相同,另外Set集合中的元素是没有下标的 3. 不可重复:指不可以存相同的元素 9. SortedSet存储元素的特点 1. 无序不可重复,但是SortedSet集合中的元素是可排序的 2. 可排序:可以按照大小进行顺序排列 ### Collection和Iterator 1. Collection接口中常用的方法 1. Collection中能存放什么类型的元素 1. 没有使用泛型之前,可以存放Object的所有子类型的元素 2. 后面使用了泛型之后,便只能存储某一个具体的类型 ```java public static void main(String[] args) { //创建一个集合对象 Collection collection = new ArrayList(); //多态 //向集合中添加元素,这里会实现自动装箱,放进去的是一个对象的内存地址 collection.add(1); collection.add(3.14); collection.add("I love xinxin"); //获取集合中元素的个数 System.out.println("集合中元素个数为: " + collection.size()); //输出: 3 //清空集合 //collection.clear(); //System.out.println("集合中元素个数为: " + collection.size()); //输出: 0 //判断当前集合是否包含某个元素 boolean b = collection.contains(3.14); System.out.println(b); //输出: true //删除集合中某个元素 collection.remove(3.14); System.out.println("集合中元素个数为: " + collection.size()); //输出: 2 //判断集合是否为空 System.out.println("当前集合是否为空: " + collection.isEmpty()); //输出: false //将集合转换为Object数组 Object[] array = collection.toArray(); for(int i = 0;i < array.length;i++){ System.out.print(array[i] + " "); } System.out.println(); } //contains方法源码解析 public static void main(String[] args) { Collection collection = new ArrayList(); //添加元素 String s1 = new String("I"); String s2 = new String("Love"); String s3 = new String("xinxin"); collection.add(s1); collection.add(s2); collection.add(s3); //判断是否包含s String s = new String("I"); //contains方法底层调用了String的equals方法 System.out.println(collection.contains(s)); //输出: true } //源码 public boolean contains(Object o) { return indexOf(o) >= 0; } public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) //这里调用equals方法进行比较,比较的是内容,所以返回true,表示包含 return i; } return -1; } //remove方法源码解析 collection.remove(s); System.out.println(collection.size()); //输出: 2 //源码分析 public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { //这里也用到了equals方法 fastRemove(index); return true; } } return false; } private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
Collection集合遍历/迭代器
- 注意:以下讲解的遍历方式/迭代方式,是所有Collection通用的一种方式,在所有的Collection以及子类中使用,在Map集合中是不能使用的
- 注意:集合结构只要发生改变,迭代器必须重新获取
- 当集合的结构发生改变时,如果还是用以前的迭代器,会出现异常:java.util.ConcurrentModificationException
public static void main(String[] args) { //创建集合对象 Collection collection = new ArrayList(); //添加元素 collection.add(1314); collection.add(520); collection.add("昕昕"); /* 集合的遍历 1. 获取迭代器Iterator对象 2. 通过该迭代器对象进行迭代/遍历 以下两个方法是迭代器对象的方法: boolean hasNext() 如果仍有元素可以迭代,则返回true Object next() 返回迭代的下一个元素 */ Iterator iterator = collection.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } }
List
List接口特有的方法
public static void main(String[] args) { //创建集合 List list = new ArrayList(); //向指定下标位置处添加元素,这个方法使用不多,因为对于ArrayList集合来说效率较低 list.add(0,"昕昕"); list.add(1,"宝贝"); list.add(2,"我爱你"); //默认向集合的尾部添加元素 list.add(1314); //迭代器遍历 Iterator iterator = list.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } //根据下标获取对应的元素 Object o = list.get(1); System.out.println(o); //输出: 宝贝 //因为有下标,所以List集合可以根据下标进行遍历(List集合特有的遍历方式,Set集合没有) for(int i = 0;i < list.size();i++){ System.out.println(list.get(i)); } //获取指定对象第一次出现处的索引 System.out.println(list.indexOf("昕昕")); //输出: 0 //获取指定对象最后一次出现处的索引 System.out.println(list.lastIndexOf("宝贝")); //输出: 1 //删除指定下标位置处的元素 Object o1 = list.remove(3); System.out.println(o1); //输出: 1314 System.out.println(list.size()); //输出: 3 //修改指定位置的元素 list.set(1,"最爱的宝贝"); for(int i = 0;i < list.size();i++){ System.out.println(list.get(i)); } }
ArrayList
public static void main(String[] args) { /* ArrayList集合: 1. 默认初始化容量:10(通过分析源代码可知,底层先创建了一个长度为0的数组,当添加第一个元素的时候,就初始化容量为10) 2. 构造方法 new ArrayList(); new ArrayList(20); 3. 集合底层是一个Object[]数组 4. ArrayList继承自AbstractList抽象类,AbstractList类继承自AbstractCollection抽象类,AbstractCollection类继承自 Collection 5. ArrayList集合的扩容 增长到原容量的1.5倍 ArrayList集合底层是数组,应该怎么优化? 尽可能少的进行扩容,因为数组扩容效率较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量 6. 数组优点: 检索效率比较高 (每个元素占用空间大小相同,内存地址也是连续的,知道了首元素的地址,然后知道下标,就可以通过数学表达式 计算出某个元素的内存地址,所以检索效率很高) 7. 数组缺点: 随机增删元素效率比较低,而且数组无法存储大数据量 8. 注意: 向数组末尾添加元素,效率很高,不受影响 9. ArrayList集合是线程不安全的 10. 面试常问的一个问题: 这么多集合当中,哪个集合用的最多? ArrayList集合用的最多,因为一般情况下,我们需要检索/查找某个元素的操作比较多 */ //三个构造方法 List list1 = new ArrayList(); //指定初始化容量为20 List list2 = new ArrayList(20); Collection c = new HashSet(); c.add(1); c.add(2); c.add(3); //通过这个构造方法就可以将HashSet集合转换为List集合 List list3 = new ArrayList(c); //二进制位运算 右移1位,其实就是除以2 左移1位,其实就是乘以2 System.out.println(10 >> 1); //输出: 5 System.out.println(10 << 1); //输出: 20 }
//源码分析
public class ArrayList<e> extends AbstractList<e> //继承AbstractList抽象类
implements List<e>, RandomAccess, Cloneable, java.io.Serializable</e></e></e>
private static final int DEFAULT_CAPACITY = 10; //初始化容量
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //无参构造会生成一个长度为0的数组,当添加第一个元素的时候,就初始化容量为10
transient Object[] elementData; //底层是Object[]数组
//无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity]; //初始化数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; //生成长度为0的数组
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//参数为集合的构造方法
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
//数组扩容 添加元素时,如果数组满了,便会实现自动扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity);
}
//真正实现扩容的方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); //位运算 将容量增长为原容量的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
3. LinkedList ```java /* LinkedList集合 1. 没有初始化容量 2. first和last引用都是null */ //源码分析 public class LinkedList<E> extends AbstractSequentialList<E> //继承自AbstractSequentialList抽象类 implements List<E>, Deque<E>, Cloneable, java.io.Serializable public abstract class AbstractSequentialList<E> extends AbstractList<E> //继承自AbstractList抽象类 transient int size = 0; //初始化大小为0 transient Node<E> first; //指向第一个结点 transient Node<E> last; //指向最后一个结点 //构造方法 public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); } //添加元素 public boolean add(E e) { linkLast(e); return true; } public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); //尾插法 else linkBefore(e, next); //头插法 nextIndex++; expectedModCount++; } //向指定下标位置插入元素 public void add(int index, E element) { checkPositionIndex(index); //判断下标是否合法 if (index == size) linkLast(element); else linkBefore(element, node(index)); } //尾插法 void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); //这里调用下面的静态内部类 last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } //头插法 void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
Vector
public static void main(String[] args) { /* Vector集合 1. 底层也是一个数组 2. 初始化容量: 10 3. 怎么扩容 扩容之后是原来容量的2倍 4. Vector中所有的方法都是线程同步的,都带有synchronized关键字 5. 如何将一个线程不安全的ArrayList转换为线程安全的Vector 可以使用集合工具类: java.util.Collections */ List list = new Vector(); list.add(1314); list.add(520); list.add("xinxin"); //遍历 Iterator iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } //ArrayList转为线程安全的Vector ArrayList list1 = new ArrayList(); list.add("xinxin"); list.add(520); //此时的list2就是线程安全的 List list2 = Collections.synchronizedList(list1); }
//源码分析
public class Vector<e> //继承自AbstractList类
extends AbstractList<e>
implements List<e>, RandomAccess, Cloneable, java.io.Serializable</e></e></e>
protected Object[] elementData; //底层Object[]数组
/**
* The amount by which the capacity of the vector is automatically
* incremented when its size becomes greater than its capacity. If
* the capacity increment is less than or equal to zero, the capacity
* of the vector is doubled each time it needs to grow.
*
* @serial
*/
protected int capacityIncrement; //数组容量 扩容时增长一倍
//构造方法
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity]; //初始化容量
this.capacityIncrement = capacityIncrement; //扩容的大小
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10); //默认初始化容量为10
}
//添加元素
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
5. 泛型机制 ```java public static void main(String[] args) { /* 泛型机制 1. JDK5.0之后推出的新特性 2. 泛型这种语法机制,只在程序的编译阶段起作用,主要是供编译器参考的(运行阶段泛型没用) 3. 使用泛型的好处 1. 集合中存储的元素类型统一了 2. 从集合中取出的元素是泛型指定的类型,不需要进行大量的"向下转型" 4. 泛型的缺点 1. 导致集合中存储的元素缺乏多样性 2. 大多数业务中,集合中元素的类型还是比较统一的,所以泛型机制还是被认可 5. JDK8.0之后,引入了自动类型推断机制,即new ArrayList<这里的类型可以不用自己写,编译器会自己推断>(); 6. 我们也可以自己自定义泛型 自定义泛型时,<>中的是一个标识符,可以随便写,但是一般情况下,我们写的是E或T */ //创建集合,只能存储String字符串 List<String> list = new ArrayList<String>(); //不能存储其他类型的元素,否则报错 list.add("1314"); list.add("xinxin"); System.out.println(list.size()); }
foreach
/* JDK5.0之后推出了一个新特性,foerach 或者叫做增强for循环 使用方式: for(元素类型 变量名 : 数组名或集合名){ System.out.println(arr[i]); } 缺点: 没右下标,在需要用到下标的循环中,不建议使用foreach */ public static void main(String[] args) { int[] arr = {1,2,3,4,5,6}; //普通for循环 for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + " "); } System.out.println(); //增强for循环 for(int i : arr){ System.out.print(i + " "); //i代表的就是数组中的每一个元素 } System.out.println(); List<String> list = new ArrayList<>(); list.add("1314"); list.add("xinxin"); list.add("520"); for(String s : list){ System.out.println(s); } }
Set
HashSet
/* HashSet集合 1. 无序且不可重复 2. 存储时顺序和取出时的顺序不一定相同 3. 存储到HashSet集合中的元素实际上是放到了HashMap集合的key部分了 */ public static void main(String[] args) { Set set = new HashSet(); //存储元素 set.add(1); set.add(2); set.add("昕昕"); set.add(1); set.add(2); //遍历,没有使用泛型,所以元素类型全为Object类型 for(Object o : set){ System.out.println(o); } } //输出 1 2 昕昕
//源码分析
public class HashSet<e> //继承自AbstractSet抽象类
extends AbstractSet<e>
implements Set<e>, Cloneable, java.io.Serializable</e></e></e>
public abstract class AbstractSet<e> extends AbstractCollection<e> implements Set<e> </e></e></e>
private transient HashMap<E,Object> map; //底层是HashMap存储结构
//构造方法
public HashSet() {
map = new HashMap<>();
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//添加元素
public boolean add(E e) {
return map.put(e, PRESENT)==null; //将元素放到map集合的key部分
}
2. TreeSet ```java /* TreeSet集合 1. 无序不可重复 2. 存储的元素可以自动按照大小顺序进行排列 3. TreeSet称为"可排序集合" 4. 存储到TreeSet集合中的元素实际上是放到了TreeMap集合的key部分了 5. TreeSet底层和TreeMap一样,都是二叉树 */ public static void main(String[] args) { Set<String> set = new TreeSet<>(); set.add("a"); set.add("z"); set.add("x"); set.add("a"); set.add("k"); //遍历 for(String s : set){ System.out.println(s); } } //输出 a k x z //源码分析 public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable public interface NavigableSet<E> extends SortedSet<E> //继承自SortedSet public interface SortedSet<E> extends Set<E> public interface NavigableMap<K,V> extends SortedMap<K,V> private transient NavigableMap<E,Object> m; //继承自SortedMap //构造方法 public TreeSet() { this(new TreeMap<E,Object>()); //底层是TreeMap存储结构 } //添加元素 public boolean add(E e) { return m.put(e, PRESENT)==null; }
Map
Map接口常用方法
/* Map接口中的常用方法 1. Map和Collection之间没有继承关系 2. Map集合以key和value的方式存储数据 3. key和value都是引用数据类型,存储的都是对象的内存地址 4. key起到的是主导地位,value只是key的一个附属品 */ public static void main(String[] args) { //创建Map集合对象 Map<Integer,String> map = new HashMap<>(); //向Map集合中添加键值对 map.put(1,"我"); map.put(2,"爱"); map.put(3,"昕昕"); //通过key获取value System.out.println(map.get(3)); //输出: 昕昕 //获取键值对的数量 System.out.println(map.size()); //输出: 3 //通过key删除某个key-value map.remove(1); System.out.println(map.size()); //输出: 2 //判断是否包含某个key System.out.println(map.containsKey(3)); //输出: true //判断是否包含某个value System.out.println(map.containsValue("爱")); //输出: true //获取所有的value Collection<String> values = map.values(); for(String s : values){ System.out.println(s); } //清空map集合 map.clear(); System.out.println(map.size()); //输出: 0 //判断map集合是否为空 System.out.println(map.isEmpty()); //输出: true }
//源码分析
public class HashMap<K,V> extends AbstractMap<K,V> //继承自AbstractMap抽象类
implements Map<K,V>, Cloneable, Serializable
transient Node<K,V>[] table; //底层存储的是Node<K,V>哈希表
transient Set<Map.Entry<K,V>> entrySet; //遍历时可将map集合转换为set集合,set集合中的元素类型为Node<K,V>
2. 遍历Map集合 ```java public static void main(String[] args) { //创建Map集合对象 Map<Integer,String> map = new HashMap<>(); //向Map集合中添加键值对 map.put(1,"我"); map.put(2,"爱"); map.put(3,"昕昕"); //第一种遍历方式: 获取所有的key,通过遍历key,来遍历value Set<Integer> keys = map.keySet(); //拿到所有的key //迭代器遍历 Iterator<Integer> iterator = keys.iterator(); while (iterator.hasNext()){ Integer key = iterator.next(); String value = map.get(key); System.out.println(key + " = " + value); } //foreach遍历 for(Integer key : keys){ String value = map.get(key); System.out.println(key + " = " + value); } //第二种方式: 把Map集合直接全部转换为Set集合,再进行遍历,此时Set集合中的元素类型为: Map.Entry //这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值,这种方式比较适用于大数据量 Set<Map.Entry<Integer, String>> set = map.entrySet(); //遍历Set集合,每一次都会取出一个Node //迭代器遍历 Iterator<Map.Entry<Integer, String>> iterator1 = set.iterator(); while(iterator1.hasNext()){ Map.Entry<Integer, String> node = iterator1.next(); Integer key = node.getKey(); String value = node.getValue(); System.out.println(key + " = " + value); } //foreach for(Map.Entry<Integer, String> node : set){ System.out.println(node.getKey() + " = " + node.getValue()); } } //源码分析 static class Node<K,V> implements Map.Entry<K,V> {//静态内部类Node<K,V>实现了Map.Entry<K,V>接口 final int hash; final K key; //键 V value; //值 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } //根据key获取value public V get(Object key) { Node<K,V> e; //每一个key-value都是一个Node return (e = getNode(hash(key), key)) == null ? null : e.value; } //获取所有的key public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; } //遍历 public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
哈希表
/* HashMap集合 1. 底层是哈希表/散列表 2. 哈希表是一种由数组和单链表结合的数据结构 3. 数组: 在查询方面效率高,随即增删方面效率较低 单链表: 在随机增删方面效率极高,在查询方面效率很低 哈希表将这两种数据结构结合在一起,充分发挥了它们各自的优点 4. HashMap集合中key的特点 1. 无序: 因为添加元素的时候并不知道会把它挂到哪个单链表上 2. 不可重复: 底层调用equals方法来保证key不重复,如果key重复了,就会将value覆盖 5. 重点: HashMap底层是必须要重写equals方法和hashCode方法的 6. 重点: 放在HashMap集合key部分的元素其实就是放在了HashSet集合中了,所以HashSet集合中的元素也需要重写hashCode方法和equals方法 7. 注意: 同一个单链表上的所有结点的hash值是相同的,因为它们的数组下标的一样的 8. HashMap集合的默认初始化容量为16,默认加载因子为0.75 这个默认加载因子是当HashMap集合底层数组的容量达到了75%的时候,就会进行数组的扩容 9. 重点: HashMap集合初始化容量时必须是2的倍数,为了达到散列均匀,提高HashMap集合的存取效率,这是必须的 10. 在JDK8.0之后,如果哈希表中的某个单链表的元素超过了8个,就会把单链表这种数据结构变成红黑树数据结构,当红黑树上的结点数量小于6时,就会把红黑树重新转换为单链表 */
//源码分析 put方法和get方法必须掌握
//构造方法
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* The default initial capacity - MUST be a power of two. //默认初始化容量必须是2的倍数
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//单链表和红黑树的互转
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
/
static final int TREEIFY_THRESHOLD = 8;
/*
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/*
map.put(k,v)实现原理
1. 先将k,v封装到Node对象中
2. 底层会调用k的hashCode()方法得到hash值,然后通过哈希函数/哈希算法,将hash值转换成数组的下标,如果此时的下标位置上没有任何元素,将把Ndoe添加到这个位置上,如果说下标对应的位置上有链表,此时就会拿着k和链表上每一个结点中的k进行equals比较,如果所有的equals方法返回值都是false,就把Node添加到链表的末尾,如果其中有一个equals方法返回了true,那么这个结点的value将会被Node结点的v覆盖
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/*
map.get(k)实现原理
1. 先调用k的hashCode()方法得出hash值,通过哈希算法将该哈希值转换为数组下标,通过数组下标快速定位到指定位置,如果这个位置是什么也没有,就返回null
2. 如果此时这个位置上有单向链表,那么就会拿着参数k和单向链表上的每个结点的k进行equals比较,如果所有的equals方法都返回false,那么get方法将返回null
3. 只要其中有一个结点的k和参数k进行equals比较后相等,那么此时这个结点的value就是我们要找的value,get方法将返回这个value
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
//同时重写hashCode()和equals()
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map)) return false; Map<?,?> m = (Map<?,?>) o; if (m.size() != size()) return false; try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false; } else { if (!value.equals(m.get(key))) return false; } } } catch (ClassCastException unused) { return false; } catch (NullPointerException unused) { return false; } return true;
}
4. HashMap和Hashtable ```java /* 1. HashMap的key和value都可以为null 2. Hashtable的key和value都不可以为null 3. Hashtable和HashMap一样,底层也是哈希表 4. Hashtable的默认初始化容量为11,默认加载因子为0.75f 5. Hashtable的扩容方式: 原容量 * 2 + 1 */ //Hashtable源码分析 public class Hashtable<K,V> //继承自Dictionary抽象类 extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable public Hashtable() { this(11, 0.75f); } private transient Entry<?,?>[] table; //put方法 public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { //value不能为null throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); //key也不能为null int index = (hash & 0x7FFFFFFF) % tab.length; //获取数组下标 @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null; } private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); //数组扩容 tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; } protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; // overflow-conscious code int newCapacity = (oldCapacity << 1) + 1; //扩容方式: 原容量 * 2 + 1 if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } } //get方法 public synchronized V get(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; }
属性类Properties
/* 对于属性类Properties,我们目前只需要掌握对象的相关方法即可 1. Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型 2. Properties类是线程安全的 3. 目前只需要掌握存和取两个方法即可 */ public static void main(String[] args) { //创建Properties类对象 Properties properties = new Properties(); //存储元素 properties.setProperty("username","xinxin"); properties.setProperty("password","1314520"); //取出元素 System.out.println(properties.getProperty("username")); System.out.println(properties.getProperty("password")); }
//源码分析
public class Properties extends Hashtable<Object,Object> //继承自Hashtable
//底层调用put(key,value)
public synchronized Object setProperty(String key, String value) {
return put(key, value);
}
//底层调用get(key)
public String getProperty(String key) {
Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}
6. TreeSet对自定义类型进行排序 ```java public static void main(String[] args) { Person p1 = new Person(10); Person p2 = new Person(14); Person p3 = new Person(9); //创建TreeSet集合 Set<Person> set = new TreeSet<>(); //添加元素,会报类型转换异常ClassCastException,原因是Person类并没有实现Comparable接口 set.add(p1); set.add(p2); set.add(p3); //遍历 for(Person p : set){ System.out.println(p); } } //自定义类 //class Person{ // private int age; // // Person(int age){ // this.age = age; // } // // @Override // public String toString() { // return "Person{" + // "age=" + age + // '}'; // } //} //实现Comparable接口,即可进行比较 class Person implements Comparable<Person>{ private int age; Person(int age){ this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "age=" + age + '}'; } //需要在这个方法中编写比较的逻辑,或者说比较的规则 @Override public int compareTo(Person p) { int age1 = this.age; int age2 = p.getAge(); if(age1 == age2){ return 0; }else if(age1 > age2){ return 1; }else{ return -1; } //直接一句话即可 //return this.age - p.getAge(); } } //源码分析 public boolean add(E e) { return m.put(e, PRESENT)==null; //调用put(k,v) } //TreeMap中的put(k,v)源码分析 private final Comparator<? super K> comparator; //TreeMap中的比较器 //构造方法 public TreeSet() { this(new TreeMap<E,Object>()); } public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); } public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; //如果调用无参构造,此时的比较器为null if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); //key不能为null @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; //必须实现Comparable接口才能向上转型为Comparable do { parent = t; cmp = k.compareTo(t.key); //使用循环将参数k的key和集合中的每一个key进行比较 if (cmp < 0) t = t.left; //往左子树进行遍历 else if (cmp > 0) t = t.right; //往右子树进行遍历 else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; }
实现比较器接口
/* TreeSet集合中实现自定义类型可排序的第二种方式: 使用比较器的方式 TreeSet和TreeMap中是有比较器属性的 最终的结论: 放到TreeSet或者TreeMap集合中key部分的元素要想做到排序,有两种方式 1. 实现Comparable接口 2. 在构造TreeSet或者TreeMap集合的时候,传进去一个比较器对象 Comparable和Comparator怎么选择? 1. 当比较规则不会发生改变的时候,或者说比较规则只有1个的时候,建议实现Comparable接口 2. 如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用比较器Comparator */ public static void main(String[] args) { //创建TreeSet集合的时候,需要使用比较器 // Set set = new TreeSet(new CatComparable()); //将比较器传进去 //或者可以使用匿名内部类的方式 Set set = new TreeSet(new Comparator<Cat>(){ @Override public int compare(Cat o1, Cat o2) { return o1.getAge() - o2.getAge(); } }); //添加元素 set.add(new Cat(10)); set.add(new Cat(12)); set.add(new Cat(9)); //遍历 for(Object o : set){ System.out.println(o); } } //自定义类,这里不实现Comparable接口 class Cat{ private int age; Cat(int age){ this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Cat{" + "age=" + age + '}'; } } //单独编写一个比较器,实现Comparator接口,也可以不写这个类,直接使用匿名内部类实现 //class CatComparable implements Comparator<Cat> { // // //比较方法 // @Override // public int compare(Cat o1, Cat o2) { // return o1.getAge() - o2.getAge(); // } // //}
Collections工具类
public static void main(String[] args) { //创建线程不安全的ArrayList List<String> list = new ArrayList<>(); //可以转换为线程安全的 List<String> list1 = Collections.synchronizedList(list); //排序 注意: 对List集合中的元素进行排序,需要保证List集合中的元素实现了Comparable接口 list.add("admin"); list.add("xinxin"); list.add("love"); Collections.sort(list); //遍历 for(String s : list){ System.out.println(s); } //对Set集合的排序,需要先将Set集合转换为List集合,再进行排序 Set<String> set = new HashSet<>(); list.add("admin"); list.add("xinxin"); list.add("love"); //转换为List List<String> list2 = new ArrayList<>(set); Collections.sort(list2); //遍历 for(String s : list2){ System.out.println(s); } }
数组
数组也是一种引用数据类型,实际上是一个容器,可以同时容纳多个相同数据类型的数据,既可以存储基本数据类型的数据,也可以存储引用数据类型的数据
数组中如果存储的是对象的话,那么存储的就是对象的"引用"(内存地址)
Java中规定,数组一旦创建,数组的长度便不可变
获取数组长度
- 数组:array.length
- 字符串:str.length()
查找元素:
数组:使用索引
字符串:使用charAt()
数组在内存方法存储的时候,数组中元素的内存地址是连续的,即存储的每一个元素都是有规则的挨着排列的
数组将首元素的内存地址作为整个数组对象的内存地址
数组中每个元素都是有下标的,从0开始,最大下标为数组长度length - 1
数组是一种数据结构,有优点,也有缺点
- 优点
- 查找元素的效率极高
- 缺点
- 添加或者删除元素的时候,需要移动数组中的一些元素,所以效率较低(对于数组中的最后一个元素,进行增删操作是没有影响的)
- 数组无法存储大数据量,因为很难在内存空间上找到一块特别大的连续的存储空间
- 优点
一维数组的声明及初始化
- 动态初始化:int []a = new int[3] 按照默认值初始化为0
- 动态初始化:int []a = new int[]{3,2,1} 长度根据元素个数确定
- 静态初始化:int []a = {3,2,1} 简化版本
什么时候采用静态初始化和动态初始化?
- 当创建数组的时候,确定数组中存储哪些具体的元素时,可以采取静态初始化
- 当创建数组的时候,不确定数组中会存储哪些数据,这个时候可以采取动态初始化,预先分配空间,再存储数据
数组扩容:先创建一个更大容量的数组,然后将当前数组的全部数据拷贝到大数组当中。注意:数组扩容效率较低,所以应该叫尽可能少的进行扩容
数组拷贝:System.arraycopy(原数组,原数组起始位置,目标数组,目标数组起始位置,需要拷贝的元素个数)
二维数组的遍历
public static void main(String[] args) { //二维数组遍历 int[][] arr = { {1,2,3}, {4,5,6}, {7,8,9} }; //双层循环遍历 for(int i = 0;i < arr.length;i++){ for(int j = 0;j < arr[i].length;j++){ System.out.print(arr[i][j] + " "); } System.out.println(); } }
一维数组模拟栈数据结构
/* 用一维数组实现栈的相应功能,包括入栈,弹栈,栈是否空,栈满等操作 */ public class ArrayTest02 { public static void main(String[] args) { MyStack stack = new MyStack(); //默认分配10个元素的内存空间 stack.push(0); stack.push(1); stack.push(2); stack.push(3); stack.push(4); stack.push(5); stack.push(6); stack.push(7); stack.push(8); stack.push(9); stack.push(10); //栈满 System.out.println(stack.pop()); //出栈 System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); } } //定义栈 class MyStack{ private Object[] stack; //定义数组 private int top; //定义栈顶指针 public MyStack() { stack = new Object[10]; //初始化,默认初始化容量为10 top = -1; } public MyStack(Object[] stack) { this.stack = stack; } public Object[] getStack() { return stack; } public void setStack(Object[] stack) { this.stack = stack; } //入栈 public void push(Object object){ if(top >= stack.length - 1){ System.out.println("栈已满,压栈失败..."); return; } top++; //每加一个元素,栈顶指针加1 stack[top] = object; System.out.println(object + "压栈成功..."); } //出栈 public Object pop(){ if(top < 0){ System.out.println("栈为空,弹栈失败..."); return null; } Object res = stack[top]; //先保留当前top指针指向的元素 top--; return res; } }
酒店管理系统(JavaSE的第一个小型项目,锻炼下面向对象)
/* 为某酒店编写程序,模拟订房,退房,打印所有房间状态等功能 1. 该系统的用户是:酒店前台 2. 酒店中所有的房间使用一个二维数组来模拟 3. 酒店中的每一个房间应该是一个Java对象 4. 每个房间都应该具有的属性:房间编号、房间类型、房间是否空闲 5. 系统对外提供的功能 a. 可以预定房间:用户输入房间编号,订房 b. 可以退房:用户输入房间编号,退房 c. 可以查看所有的房间状态:用户输入某个指令,应该可以查看所有房间的状态 */
Arrays工具类的使用
//最常用的两个方法:排序和二分查找(其他方法可查阅JDK1.8文档) public static void main(String[] args) { int[] arr = {1,3,6,4,5,7,10}; //排序 Arrays.sort(arr); //主要使用了优化的快速排序算法 for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + " "); } System.out.println(); //二分查找 int index = Arrays.binarySearch(arr, 4); System.out.println(index == -1 ? "该元素不存在" : "该元素的下标为: " + index); }
//将该数字里面的所有元素都赋值为该值 Arrays.fill(数组名,值) //利用Arrays类的toString()方法,返回一个包含数组元素的字符串,这些元素被放置在括号内,并用逗号进行分隔 int []array = new int [] {12,19,2,29,36,18,9}; System.out.println(Arrays.toString(array)); //如果希望将一个数组的所有值全部拷贝到一个新的数组中,可以使用Arrays类的copyOf方法 int []array = new int [] {12,19,2,29,36,18,9}; System.out.println(Arrays.toString(array)); int []newArray = Arrays.copyOf(array, array.length); for(int i:newArray) { System.out.print(i+" "); } //(第二个参数为数组长度,这个方法通常用来增加数组大小,如果数组元素是数值型,那么多余的元素就被赋值为0,相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素) int []newArray = Arrays.copyOf(array, 2*array.length); //注意:在Java中,允许将一个数组变量拷贝给另一个数组变量,这时两个变量将引用同一个数组 //返回与a类型相同的一个数组,其长度为end-start Arrays.copyOfRange(type[]a,int start,int end) //如果两个数组的大小相同,而且下标相同的元素都对应相等,则返回true static boolean equals(type []a,type []b) //快速地打印一个二维数组的数据元素列表 System.out.print(Arrays.deepToString(array)); ```
两种排序
public static void main(String[] args) { int[] arr1 = {3,1,5,9,6,8,4}; //冒泡排序 bubbleSort(arr1); print(arr1); System.out.println(); int[] arr2 = {3,1,5,9,6,8,4}; //选择排序 selectSort(arr2); } //冒泡排序 public static void bubbleSort(int[] arr){ int temp = 0; for(int i = 0;i < arr.length - 1;i++){ //循环次数:数组长度 - 1 for(int j = 0;j < arr.length - 1 - i;j++){ if(arr[j] > arr[j + 1]){ temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } /* 选择排序 1. 每一次从"这堆"参与比较的数据当中找出最小值 2. 然后将这个最小值和"这堆"最前面的元素进行交换 选择排序比冒泡排序好的地方:每一次的交换位置都是有意义的 */ public static void selectSort(int[] arr){ int temp = 0; for(int i = 0;i < arr.length - 1;i++){ //循环次数:数组长度 - 1 int min = i; //假设i下标位置上的元素值是最小的 for(int j = i + 1;j < arr.length;j++){ if(arr[j] < arr[min]){ min = j; //将更小的元素的下标赋给min } } //如果比较完之后,min还是i,说明此时下标为i的元素值就是最小的,就不需要进行交换,否则就要交换 if(min != i){ temp = arr[min]; arr[min] = arr[i]; arr[i] = temp; } } } //打印数组 public static void print(int[] arr){ for(int i = 0;i < arr.length;i++){ System.out.print(arr[i] + " "); } }
二分查找
public static void main(String[] args) { int[] arr = {1,3,5,7,9}; int index = primarySearch(arr,5); System.out.println(index == -1 ? "该元素不存在" : "该元素的下标为: " + index); index = binarySearch(arr,9); System.out.println(index == -1 ? "该元素不存在" : "该元素的下标为: " + index); } //普通查找 public static int primarySearch(int[] arr,int val){ for(int i = 0;i < arr.length;i++){ if(arr[i] == val){ return i; } } return -1; } //二分查找:要求数组必须有序 public static int binarySearch(int[] arr,int val){ int begin = 0; //开始下标 int end = arr.length - 1; //结束下标 while(begin <= end){ int mid = (begin + end) / 2; //中间下标 if(arr[mid] == val){ return mid; }else if(arr[mid] < val){ begin = mid + 1; }else{ end = mid - 1; } } return -1; }
/* 写一个类Army,代表一支军队,这个类有一个属性Weapon数组w(用来存储该军队所拥有的所有武器),该类还提供了一个构造方法,在构造方法里通过传一个int类型的参数来限定该类所能拥有的最大武器数量,并用这一大小来初始化数组w 该类还提供一个方法addWeapon(Weapon wa),表示把参数wa所代表的武器放入到数组w中 在这个类中还定义方法attackAll()让数组w中的所有武器进行攻击 以及moveAll()让数组w中的所有可移动的武器进行移动 */ public static void main(String[] args) { //创建军队,该军队有四个武器 Army army = new Army(4); //创建武器对象 Tank tank1 = new Tank(); Tank tank2 = new Tank(); Pao pao1 = new Pao(); Pao pao2 = new Pao(); //添加武器 try { army.addWeapon(tank1); army.addWeapon(tank2); army.addWeapon(pao1); army.addWeapon(pao2); } catch (MyException e) { e.printStackTrace(); } //让所有可移动的进行移动 army.moveAll(); //让所有可攻击的进行攻击 army.attackAll(); } //移动接口 interface Moveable{ void move(); //移动 } //攻击接口 interface Shootable{ void shoot(); //攻击 } //武器类 class Weapon{ } //坦克类 class Tank extends Weapon implements Shootable,Moveable{ @Override public void move() { System.out.println("坦克在移动..."); } @Override public void shoot() { System.out.println("坦克在攻击..."); } } //大炮类 class Pao extends Weapon implements Shootable{ @Override public void shoot() { System.out.println("开炮..."); } } //军队类 class Army{ private Weapon[] w; public Army(int count){ w = new Weapon[count]; //初始化 } //将武器加入数组 public void addWeapon(Weapon wa) throws MyException { for(int i = 0;i < w.length;i++){ if(null == w[i]){ w[i] = wa; return; } } //程序如果执行到这,说明武器已经达到上限 throw new MyException("武器数量已达到上限..."); //抛出异常 } //所有可攻击的武器进行攻击 public void attackAll(){ //遍历数组 for(int i = 0;i < w.length;i++){ if(w[i] instanceof Shootable){ //说明该武器是可攻击的 Shootable shootable = (Shootable)w[i]; shootable.shoot(); } } } //所有可移动的武器进行移动 public void moveAll(){ //遍历数组 for(int i = 0;i < w.length;i++){ if(w[i] instanceof Moveable){ //说明该武器是可移动的 Moveable moveable = (Moveable)w[i]; moveable.move(); } } } }
IO流
通过IO可以完成对硬盘文件的读和写
IO流的分类
按照流的方向进行分类,以内存作为参照物
- 往内存中去,叫做输入(Input)或者叫做读(Read)
- 从内存中出来,叫做输出(Output)或者叫做写(Write)
按照读取数据方式不同进行分类
- 有的流是按照字节的方式进行读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流的读取是万能的,即什么类型的文件都可以读取,包括: 文本文件、图片、声音文件、视频文件等
- 有的流是按照字符的方式进行读取数据,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流无法读取图片、声音文件、视频文件等,只能读取纯文本文件,连word文档都无法读取
- 综上所述,流的分类:
- 输入流、输出流
- 字节流、字符流
IO流的四大家族的首领,都是抽象类
- InputStream 字节输入流
- OutputStream 字节输出流
- Reader 字符输入流
- Writer 字符输出流
- 注意: Java中只要类名以"Stream"结尾的都是字节流,以"Reader/Writer"结尾的都是字符流
- 所有的流(包括输入输出流)都实现了java.io.Closeable接口,都是可关闭的,都实现了close()方法
- 流毕竟是内存和硬盘之间的一个通道,所以用完之后一定要记得关闭,养成好习惯,否则会耗费(占用)很多资源
- 所有的输出流都实现了java.io.Flushable接口,都是可刷新的,都有flush()方法。输出流在最终输出之后,一定要记得调用flush()刷新一下,这个刷新表示将管道当中剩余未输出的数据强行输出完,也就是说要清空管道,如果没有调用flush(),可能会导致部分数据丢失
我们需要掌握的流有16个
- 文件专属
- FileInputStream 必须掌握
- FileOutputStream 必须掌握
- FileReader
- FileWriter
- 转换流(将字节流转换为字符流)
- InputStreamReader
- OutputStreamWriter
- 缓冲流专属
- BufferedReader
- BufferedWriter
- BufferedInputStream
- BufferedOutputStream
- 数据流专属
- DataInputStream
- DataOutputStream
- 标准输出流
- PrintWriter
- PrintStream 必须掌握
- 对象专属流
- ObjectInputStream 必须掌握
- ObjectOutputStream 必须掌握
- 文件专属
FileInputStream
/* 1. 文件字节输入流,任何文件都可以采用这个流来读 2. 字节的方式,完成输入的操作(硬盘--->内存) */ //第一种方式: 读取单个字节 public static void main(String[] args) { //创建文件字节输入流对象 FileInputStream inputStream = null; try { inputStream = new FileInputStream("D:\\IO流测试\\demo1.txt"); //读取文件 int read = inputStream.read(); //返回值是读取到的字节 System.out.println(read); //输出: 97 read = inputStream.read(); System.out.println(read); //输出: 98 } catch (IOException e) { e.printStackTrace(); }finally { //关闭流 if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
//使用循环读取 public static void main(String[] args) { //创建文件字节输入流对象 FileInputStream inputStream = null; try { inputStream = new FileInputStream("D:\\IO流测试\\demo1.txt"); //读取文件,这里使用循环读 int read = 0; // while(true){ // read = inputStream.read(); // if(read == -1){ //如果没数据了,就会返回-1 // break; // } // System.out.println(read); // } //改造while循环 while((read = inputStream.read()) != -1){ System.out.println(read); } } catch (IOException e) { e.printStackTrace(); }finally { //关闭流 if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } //以上程序的缺点: 一次读取一个字节,这样内存和硬盘交互太频繁了,基本上时间资源都耗费在交互上面了所以第二种方式是: 往byte[]数组上面读 public static void main(String[] args) { FileInputStream inputStream = null; try { //这里我们可以写相对路径,相对路径是从当前所在的位置作为起点开始找的 //IDEA默认的当前路径: 工程Project的根路径 inputStream = new FileInputStream("demo"); //读取文件,往byte[]数组中读取,一次读取多个字节,最多读取"数组.length"个字节 byte[] bytes = new byte[4]; //这个方法的返回值是: 读取到的字节的数量 int i = inputStream.read(bytes); System.out.println(i); //输出: 4 //将byte[]数组全部转为字符串 //System.out.println(new String(bytes)); //输出: abcd //不应该全部转换,而应该是读取到了多少个字节,就转换多少个,其他的不需要转 System.out.println(new String(bytes,0,i)); i = inputStream.read(bytes); System.out.println(i); //输出: 2 //将byte[]数组全部转为字符串 //System.out.println(new String(bytes)); //输出: efcd 这个输出是有问题的,cd是不需要输出的 System.out.println(new String(bytes,0,i)); } catch (IOException e) { e.printStackTrace(); }finally { if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } //最终整合: 需要掌握 public static void main(String[] args) { FileInputStream inputStream = null; try { inputStream = new FileInputStream("demo"); byte[] bytes = new byte[4]; //第一种循环方式 while(true){ int readCount = inputStream.read(bytes); if(readCount == -1){ break; } System.out.print(new String(bytes,0,readCount)); } //第二种循环方式 int readCount = 0; while((readCount = inputStream.read(bytes)) != -1){ System.out.print(new String(bytes,0,readCount)); } } catch (IOException e) { e.printStackTrace(); } } /* FileInputStream类的其他常用方法 1. int available() 返回流当中剩余的没有读到的字节数量 2. long skip(long n) 跳过几个字节不读 */ public static void main(String[] args) { FileInputStream inputStream = null; try { inputStream = new FileInputStream("demo"); System.out.println("总字节数量: " + inputStream.available()); //输出: 6 //读取一个字节 int readByte = inputStream.read(); //还剩下可以读取的字节数量: 5 System.out.println("还剩下多少个字节没有读: " + inputStream.available()); //输出: 5 //这个方法的作用: 我们定义byte[]数组的时候,就可以直接准确的定义数组大小,读一次即可 //注意: byte[]数组读取的这种方式不太适合太大的文件,因为byte[]数组不能太大 byte[] bytes = new byte[inputStream.available()]; //此时的数组大小为6 int read = inputStream.read(bytes); System.out.println(new String(bytes)); //输出: bcdef //skip方法 inputStream.skip(3); //跳过abc,直接输出d System.out.println(inputStream.read()); //输出: 100 } catch (IOException e) { e.printStackTrace(); }finally { if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } ```
FileOutputStream
/* 文件字节输出流,负责写 从内存到硬盘 */ public static void main(String[] args) { FileOutputStream outputStream = null; try { //注意: 如果当前文件不存在,编译器会自动帮你创建文件, //但是这种方式谨慎使用,因为第二次运行程序写入的时候,会先将原文件清空,然后重新写入 //outputStream = new FileOutputStream("demo1"); //以追加的方式在文件末尾写入,不会清空原文件内容 outputStream = new FileOutputStream("demo2",true); //开始写 byte[] bytes = {97,98,99,100}; //将整个byte[]数组全部写出 //outputStream.write(bytes); //写入abcd //将byte[]数组部分写出 //outputStream.write(bytes,0,2); //写出ab //也可以写字符串 String s = "我爱昕昕"; byte[] bytes1 = s.getBytes(); outputStream.write(bytes1); //最后: 一定要记得刷新 outputStream.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(outputStream != null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
文件赋值
/* 1. 使用FileInputStream + FileOutputStream完成文件的拷贝 2. 拷贝的过程应该是一边读一边写 3. 使用以上的字节流拷贝文件的时候,文件类型随意,即什么样的文件类型都可以拷贝 */ public static void main(String[] args) { FileInputStream inputStream = null; FileOutputStream outputStream = null; try { //创建输入流对象 inputStream = new FileInputStream("D:\\IO流测试\\demo1.txt"); //创建输出流对象 outputStream = new FileOutputStream("D:\\IO流测试\\Test\\demo1.txt"); //重点: 一边读一边写 byte[] bytes = new byte[1024]; //一般设置为1024的倍数 int readCount = 0; while((readCount = inputStream.read(bytes)) != -1){ outputStream.write(bytes,0,readCount); } //刷新输出流 outputStream.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(outputStream != null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
FileReader
/* 1. 文件字符输入流,只能读取普通文本文件 2. 读取文本内容时,比较方便快捷 */ public static void main(String[] args) { FileReader reader = null; try { reader = new FileReader("demo3"); //开始读 char[] chars = new char[4]; //一次只读取4个字符 int readCount = 0; while((readCount = reader.read(chars)) != -1){ System.out.println(new String(chars,0,readCount)); } } catch (IOException e) { e.printStackTrace(); }finally { if(reader != null){ try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
FileWriter
/* 1. 文件字符输出流,负责写 2. 只能输出普通文本 */ public static void main(String[] args) { FileWriter writer = null; try { //如果当前文件不存在,编译器会帮我们创建 writer = new FileWriter("demo4"); //开始写 char[] chars = {'我','爱','昕','昕'}; //将整个char[]数组写进去 writer.write(chars); //将char[]数组的一部分写进去 writer.write(chars,2,2); //也可以直接写字符串 writer.write("依依目光,此生不换"); } catch (IOException e) { e.printStackTrace(); }finally { if(writer != null){ try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } }
复制普通文本文件
/* 使用FileReader + FileWriter进行拷贝的话,只能拷贝"普通文本文件" */ public static void main(String[] args) { FileReader reader = null; FileWriter writer = null; try { //读取 reader = new FileReader("D:\\IO流测试\\demo2.txt"); //写入 writer = new FileWriter("D:\\IO流测试\\Test\\demo2.txt"); //一边读一边写 char[] chars = new char[1024]; int readCount = 0; while((readCount = reader.read(chars)) != -1){ writer.write(new String(chars,0,readCount)); } //刷新 writer.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(writer != null){ try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } if(reader != null){ try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
带有缓冲区的字符输入流
/* 1. 带有缓冲区的字符输入流 2. 使用这个流的时候不需要自定义char[]数组,也不需要自定义byte[]数组,自带缓冲 */ public static void main(String[] args) { FileReader reader = null; BufferedReader bufferedReader = null; try { reader = new FileReader("demo4"); //当一个流的构造方法中需要使用到另一个流的时候,这个被传进来的参数流叫做: 节点流 //外部负责包装的这个流叫做: 包装流/处理流 //当前这个FileReader就是一个节点流,BufferedReader就是包装流/处理流 bufferedReader = new BufferedReader(reader); //读取的时候读取一行,但是不带换行符,我们需要自己手写换行符 String s = null; while((s = bufferedReader.readLine()) != null){ System.out.println(s); } } catch (IOException e) { e.printStackTrace(); }finally { //注意: 这里只需要关闭外部的包装流,里面的节点流会自动关闭(看源码就可以知道) if(bufferedReader != null){ try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
//源码分析 public class BufferedReader extends Reader //继承自Reader private Reader in; private static int defaultCharBufferSize = 8192; //缓冲区默认大小 //构造方法 public BufferedReader(Reader in) { this(in, defaultCharBufferSize); //调用默认的defaultCharBufferSize } public BufferedReader(Reader in, int sz) { super(in); if (sz <= 0) throw new IllegalArgumentException("Buffer size <= 0"); this.in = in; //将我们传入的Reader对象赋值给属性in cb = new char[sz]; nextChar = nChars = 0; } //close方法 public void close() throws IOException { synchronized (lock) { if (in == null) return; try { in.close(); //关闭节点流对象 } finally { in = null; cb = null; } } } ```
转换流
/* 将字节流转换为字符流 */ public static void main(String[] args) { FileInputStream inputStream = null; BufferedReader bufferedReader = null; InputStreamReader reader = null; try { //字节流 inputStream = new FileInputStream("demo4"); //通过转换流将字节流转换为字符流 reader = new InputStreamReader(inputStream); //这个构造方法只能传一个字符流,不能传字节流,所以需要转换 bufferedReader = new BufferedReader(reader); //读取 String line = null; while((line = bufferedReader.readLine()) != null){ System.out.println(line); } } catch (IOException e) { e.printStackTrace(); }finally { if(bufferedReader != null){ try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
带有缓冲区的字符输出流
public static void main(String[] args) { BufferedWriter bufferedWriter = null; try { bufferedWriter = new BufferedWriter(new FileWriter("demo5")); //开始写 bufferedWriter.write("我爱昕昕"); bufferedWriter.write("\n"); bufferedWriter.write("昕昕爱我"); //刷新 bufferedWriter.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(bufferedWriter != null){ try { bufferedWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } }
数据流
/* 1. DataOutputStream: 数据专属的流 2. 这个流可以将数据连同数据的类型一并写入文件中 3. 注意: 这个文件不是普通的文本文件,使用记事本是打不开的 4. DataOutputStream写的文件只能使用DataInputStream去读,而且读的时候你需要提前知道写入的顺序,读取的顺序需要和写入的顺序一致,才可以正常取出数据 */ public static void main(String[] args) { //创建数据专属的字节输出流 DataOutputStream dos = null; FileOutputStream outputStream = null; try { outputStream = new FileOutputStream("data"); dos = new DataOutputStream(outputStream); //写数据,将数据及数据类型一并写入到文件当中 dos.writeByte(1); dos.writeShort(3); dos.writeInt(1); dos.writeLong(4); dos.writeFloat(5.20f); dos.writeDouble(1314520); dos.writeBoolean(true); dos.writeChar('x'); //刷新 dos.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(dos != null){ try { dos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
/* DataInputStream: 数据字节输入流 */ public static void main(String[] args) { DataInputStream dis = null; try { dis = new DataInputStream(new FileInputStream("data")); //开始读 System.out.println(dis.readByte()); System.out.println(dis.readShort()); System.out.println(dis.readInt()); System.out.println(dis.readLong()); System.out.println(dis.readFloat()); System.out.println(dis.readDouble()); System.out.println(dis.readBoolean()); System.out.println(dis.readChar()); } catch (IOException e) { e.printStackTrace(); }finally { if(dis != null){ try { dis.close(); } catch (IOException e) { e.printStackTrace(); } } } } ```
标准输出流
/* PrintStream: 标准的字节输出流,默认输出到控制台 */ public static void main(String[] args) throws FileNotFoundException { //创建标准的字节输出流,不需要手动close()关闭 PrintStream stream = System.out; //默认往控制台打印 stream.println("xinxin"); stream.println(1314); //我们可以自定义一个PrintStream,然后修改输出方向,使输出指向"log"文件 PrintStream stream1 = new PrintStream(new FileOutputStream("log")); System.setOut(stream1); //此时的输出会放到log文件中 System.out.println("hello world"); System.out.println("hello xinxin"); }
//日志工具类 public class Logger { //记录日志的方法 public static void log(String msg){ //指向一个日志文件 try { PrintStream out = new PrintStream(new FileOutputStream("log.txt"),true); //改变输出方向 System.setOut(out); //日期当前时间 Date date = new Date(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); String str = format.format(date); System.out.println(str + " : " + msg); } catch (FileNotFoundException e) { e.printStackTrace(); } } } public static void main(String[] args) { //写入日志 Logger.log("余生只愿为你"); Logger.log("我爱昕昕"); Logger.log("世界这么大还是遇见你"); } ```
File类的常用方法
/* 1. File对象有可能对应的是目录,也有可能对应的是文件,File只是一个路径名的抽象表示形式 2. File类和IO的四大家族没有关系,所以File类是无法完成文件的读取和写入操作的 3. 我们需要掌握File类中常用的方法 */ public static void main(String[] args) throws IOException { //创建File对象 File file = new File("D:\\IO流测试\\File\\test"); //判断该文件是否存在 System.out.println(file.exists()); //输出: false //如果该文件不存在,我们就以文件的形式将它创建出来 // if(!file.exists()){ // file.createNewFile(); // } //如果该文件不存在,则以目录的形式创建出来 if(!file.exists()){ file.mkdir(); } //创建多重目录 // File file1 = new File("D:\\IO流测试\\File\\a\\b\\c"); // if(!file1.exists()){ // file1.mkdirs(); // } //获取父目录和文件的绝对路径 File file2 = new File("D:\\IO流测试\\File"); String parentPath = file2.getParent(); //得到一个路径 System.out.println(parentPath); File parentFile = file2.getParentFile(); //得到一个File System.out.println("获取绝对路径: " + parentFile.getAbsolutePath()); }
public static void main(String[] args) { File file = new File("D:\\IO流测试\\File\\test"); //获取文件名 System.out.println("文件名为: " + file.getName()); //判断是否是一个目录 System.out.println(file.isDirectory()); //判断是否是一个文件 System.out.println(file.isFile()); //获取文件最后一次修改时间 long l = file.lastModified(); //这个时间是从1970年到现在的总毫秒数 //将总毫秒数转换为日期 Date date = new Date(l); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); String time = sdf.format(date); System.out.println(time); //获取文件的大小,单位为字节 System.out.println(file.length()); } //测试File类中的listFiles方法 public static void main(String[] args) { //创建File对象 File file = new File("D:\\IO流测试"); //获取当前目录下的所有子文件 File[] files = file.listFiles(); for(File f : files){ System.out.println(f.getName()); } } ```
目录拷贝
public static void main(String[] args) { //拷贝源 File srcFile = new File("D:\\IO流测试"); //拷贝目标 File destFile = new File("F:\\test2"); //调用拷贝方法 copyFile(srcFile,destFile); } private static void copyFile(File srcFile, File destFile) { //如果是文件的话,就不需要进行递归,直接拷贝过去即可 if(srcFile.isFile()){ //一边读一边写 FileInputStream inputStream = null; FileOutputStream outputStream = null; try { inputStream = new FileInputStream(srcFile); String path = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcFile.getAbsolutePath().substring(3); outputStream = new FileOutputStream(path); byte[] bytes = new byte[1024 * 1024]; //一次复制1MB int readCount = 0; while((readCount = inputStream.read(bytes)) != -1){ outputStream.write(bytes,0,readCount); } //刷新 outputStream.flush(); } catch (IOException e) { e.printStackTrace(); }finally { if(outputStream != null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if(inputStream != null){ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return; } //获取下面所有的子目录 File[] files = srcFile.listFiles(); //遍历 for(File file : files){ //如果此时是目录 if(file.isDirectory()){ String srcDir = file.getAbsolutePath(); //源目录的绝对路径 //目标目录的绝对路径 String destDir = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcDir.substring(3); File newFile = new File(destDir); //如果不存在,就创建多目录 if(!newFile.exists()){ newFile.mkdirs(); } } //递归获取 copyFile(file,destFile); } }
序列化的实现
/* 1. 参与序列化与反序列化的对象,必须实现Serializable接口 2. 注意: 通过源代码可以发现,Serializable接口只是一个标志性的接口,里面没有任何代码 3. Serializable接口的作用 起到标识的作用,JVM看到这个类实现了Serializable接口,可能会对这个类有特殊待遇 JVM看到这个接口之后,会为该类自动生成一个序列化版本号 4. 序列化版本号的作用 1. Java是采用什么样的机制来区分不同类的 1. 首先通过类名进行比对,如果类名不一样,肯定不是同一个类 2. 如果类名一样,就会靠序列化版本号来进行区分 2. 缺陷 1. 这种自动生成的序列化版本号的缺点: 一旦代码确定之后,便无法进行后续的修改,因为只要一修改,JVM必然会重新编译,此时就会生成全新的序列化版本号,这个时候JVM会认为这是一个全新的类,那后面反序列化这个类的时候就会报错,所以最好自己手动写序列化版本号 5. 最终结论 只要一个类实现了Serializable接口,建议给该类手动提供一个固定不变的序列化版本号,这样即使以后这个类的代码修改了,但是版本号不变,JVM还是会认为这是同一个类 */ public static void main(String[] args) throws IOException { Student s = new Student(1,"昕昕"); ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("students")); //序列化 outputStream.writeObject(s); //刷新 outputStream.flush(); //关闭 outputStream.close(); } //实现Serializable接口 class Student implements Serializable { //JVM看到这个Serializable接口之后,会自动生成一个序列化版本号,这里没有手动写出来,JVM会默认提供这个序列化版本号,建议将序列化版本号手动写出来,不建议自动生成 private static final long serialVersionUID = 1L; //IDEA自动生成 //private static final long serialVersionUID = -1113668179596373415L; private int no; private String name; public Student() { } public Student(int no, String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student{" + "no=" + no + ", name='" + name + '\'' + '}'; } } public static void main(String[] args) throws Exception { ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("students")); //反序列化 Object o = inputStream.readObject(); System.out.println(o); //关闭 inputStream.close(); }
/* 一次序列化多个对象 可以将多个对象放到集合当中,序列化集合 如果不用集合,直接将多个对象进行序列化,当序列化第二个对象的时候就会报错,所以需要使用List 注意: 集合和集合中的对象,都要实现Serializable接口 */ public static void main(String[] args) throws IOException { List<User> list = new ArrayList<>(); list.add(new User("1314","xinxin")); list.add(new User("520","昕昕")); ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("users")); //序列化一个集合 outputStream.writeObject(list); //刷新 outputStream.flush(); //关闭 outputStream.close(); } class User implements Serializable { private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } 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; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + '}'; } } public static void main(String[] args) throws Exception { ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("users")); //反序列化集合 Object o = inputStream.readObject(); System.out.println(o instanceof List); //输出: true //实现强转 List<User> list = (List<User>)o; //遍历 for(User user : list){ System.out.println(user); } } ```
transient关键字
class User implements Serializable { //transient关键字表示游离的,即被transient修饰的属性不参与序列化操作 private transient String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } }
IO和属性类Properties联立使用
/* IO + Properties的联合使用,是一个非常好的设计理念 以后经常需要改动的数据,可以单独写到一个文件中,使用程序进行动态读取,将来只需要修改这个文件的内容,不需要改动Java代码,也不需要重新编译,服务器也不需要重启,就可以拿到动态的信息 */ public static void main(String[] args) throws Exception { /* Properties是一个Map集合,key和value都是String类型 要求: 动态获取info文件中的内容到Properties对象当中 */ //新建一个输入流对象 FileReader reader = new FileReader("info"); //新建一个Properties对象 Properties pro = new Properties(); //调用Properties对象的load方法将文件中的数据加载到Properties集合当中 pro.load(reader); //获取数据 String username = pro.getProperty("username"); String password = pro.getProperty("password"); System.out.println("username: " + username + " password: " + password); //关闭 reader.close(); }
高并发(多线程)
进程和线程的概念
进程
进程就是一个应用程序
线程
线程是一个进程中执行场景/执行单元
一个进程可以启动多个线程
当启动一个Java程序的时候,会先启动JVM,而JVM就是一个进程,JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护回收垃圾,所以这个时候最起码已经有了两个并发线程
进程和线程的关系
- 进程可以看做是现实生活当中的公司
- 线程可以看做是公司当红的某个员工
- 注意
- 进程A和进程B的内存相互独立不共享
- 在Java中,线程A和线程B的堆内存和方法区内存是共享的,但是栈内存是独立的,一个线程对应一个栈内存
- Java中之所以有多线程并发机制,目的是为了提高程序的处理效率
多线程并发
- 对于单核计算机来说,是无法真正的实现多线程并发的。虽然不能做到真正的多线程并发,但是可以给人一种"多线程并发"的感觉,对于单核的cpu来说,在某一个时间点上实际上只能处理一件事情,但是由于cpu的处理速度极快,多个线程之间频繁切换执行,给人的感觉就是: 多个事情同时在做
- 对于多核的计算机来说,是可以真正做到几个线程同时并发执行的
实现线程的第一种方式
/* 1. 编写一个类直接继承Thread类,然后重写run方法 2. run()方法和start()方法的区别 start()方法的任务是开辟一个新的栈空间,只要空间开辟完了,start()方法就结束了 run()方法是不需要手动调用的,是由JVM线程调度机制来运作的 */ public static void main(String[] args) { //创建分支线程对象 MyThread thread = new MyThread(); /* 启动线程 start()方法的作用: 启动一个分支线程,在JVM中开辟一个新的栈空间,这行代码任务完成之后,瞬间就结束了 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来了,start()方法就结束了,分支线程就会启动成功 启动成功的分支线程就会自动调用run()方法,所以run()方法必须要实现 */ thread.start(); //这里如果调用run()方法,那只是单纯的方法调用,此时还是单线程 //thread.run(); //这里的代码还是运行在主线程main当中的 for(int i = 0;i < 1000;i++){ System.out.println("主线程--->" + i); } } class MyThread extends Thread{ @Override public void run() { for(int i = 0;i < 1000;i++){ System.out.println("分支线程--->" + i); } } }
实现线程的第二种方式
/* 1. 编写一个类,实现Runnable接口,实现run()方法 2. 这种方式使用的比较多,因为实现了Runnable接口之后,将来还可以继承其他的类,更加灵活 */ public static void main(String[] args) { //创建一个可运行的对象 MyRunnable run = new MyRunnable(); //将可运行的对象通过构造方法封装成一个线程对象 Thread t = new Thread(run); t.start(); for(int i = 0;i < 100;i++){ System.out.println("主线程--->" + i); } } //这并不是一个线程类,只是一个可运行的类 class MyRunnable implements Runnable{ @Override public void run() { for(int i = 0;i < 100;i++){ System.out.println("分支线程--->" + i); } } }
采用匿名内部类的方式实现线程
public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { for(int i = 0;i < 100;i++){ System.out.println("分支线程--->" + i); } } }); //启动线程 t.start(); for(int i = 0;i < 100;i++){ System.out.println("主线程--->" + i); } }
- 线程的生命周期
获取线程的名字
public static void main(String[] args) { MyThread2 t = new MyThread2(); //获取线程的名字,默认名称: Thread-0 System.out.println("线程名字为: " + t.getName()); //输出: Thread-0 //设置线程名字 t.setName("线程A"); System.out.println("线程名字为: " + t.getName()); //输出: 线程A //启动线程 t.start(); } class MyThread2 extends Thread{ @Override public void run() { for(int i = 0;i < 100;i++){ System.out.println("分支线程--->" + i); } } }
获取当前线程对象
public static void main(String[] args) { MyThread3 t = new MyThread3(); t.setName("线程x"); //获取当前线程 Thread thread = Thread.currentThread(); System.out.println(thread.getName()); //输出: main //启动线程 t.start(); } class MyThread3 extends Thread{ @Override public void run() { for(int i = 0;i < 100;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
线程的sleep方法
/* 1. 静态方法 2. 参数是毫秒 3. 作用: 让当前线程进入休眠,进入"阻塞状态",放弃当前占有的cpu时间片,让给其他线程使用 */ public static void main(String[] args) { //让当前的主线程休眠5秒后再运行 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello world"); //间隔特定的时间去执行一段特定的代码,每隔多久执行一次 for(int i = 0;i < 10;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); //每输出一个数,就休眠一秒 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
//关于sleep方法的一道面试题 public static void main(String[] args) { //创建线程对象 Thread t = new MyThread4(); t.setName("线程A"); //启动线程 t.start(); //调用sleep方法 try { /* 注意: 这行代码并不会让线程A休眠5秒 因为sleep方法是静态方法,属于类,跟对象没关系,所以执行的时候还是会转为Thread.sleep(1000 * 5) 它会让当前线程休眠5秒,也就是说会让主线程休眠5秒,而并不会影响线程A */ t.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } //5秒之后才会执行这行代码 System.out.println("hello world"); } class MyThread4 extends Thread{ @Override public void run() { for(int i = 0;i < 10;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); } } } ```
终止线程的睡眠,唤醒线程
public static void main(String[] args) { Thread t = new Thread(new MyRunnable2()); t.setName("线程x"); t.start(); //希望5秒之后,线程x苏醒,不再睡眠 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } //终止睡眠 //注意: 这里终止线程x的睡眠,依靠的是Java的异常处理机制,让它报异常,从而苏醒 t.interrupt(); } class MyRunnable2 implements Runnable{ //重点: run()方法当中的异常不能使用throws抛出,只能使用try catch捕捉 //因为run()方法在父类中并没有抛出任何异常,所以子类不能比父类抛出更多的异常 @Override public void run() { System.out.println(Thread.currentThread().getName() + "---> begin"); try { //睡眠一年 Thread.sleep(1000 * 60 * 60 * 24 * 365); //通过异常,让它终止睡眠 } catch (InterruptedException e) { e.printStackTrace(); //这里会打印异常信息 } //1年之后再执行这行语句 System.out.println(Thread.currentThread().getName() + "---> end"); } }
强行终止线程的执行
/* 这种方式存在很大的缺点: 容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失,不建议使用 */ public static void main(String[] args) { Thread t = new Thread(new MyRunnable3()); t.setName("线程x"); t.start(); //休眠5秒 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } //5秒之后,强行终止线程x t.stop(); //该方法已过时,不建议使用 }
class MyRunnable3 implements Runnable{ @Override public void run() { for(int i = 0;i < 10;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { //每输出一个数就休眠一秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } ```
合理的终止某一个线程
/* 合理的终止一个线程的执行,这种方式是很常用的 */ public static void main(String[] args) { MyRunnable4 r = new MyRunnable4(); Thread t = new Thread(r); t.setName("线程x"); t.start(); //休眠5秒 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } //终止线程x r.flag = false; }
class MyRunnable4 implements Runnable{ //可以自己手写一个标记,根据该标记来合理判断是否需要终止当前线程 boolean flag = true; @Override public void run() { for(int i = 0;i < 10;i++){ if(flag){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { //休眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }else{ //return语句结束当前方法,也就是结束当前线程 return; } } } } ```
线程调度
- 常见的线程调度模型有哪些
- 抢占式调度模型
- 哪个线程的优先级高,抢到cpu的时间片的概率就会高一些
- Java采用的就是抢占式调度模型
- 均分式调度模型
- 平均分配cpu的时间片,每个线程占有的cpu时间片长度一样
- 特点: 平均分配,一切平等
- 抢占式调度模型
- Java中提供了哪些方法是和线程调度有关系的呢?
- 实例方法
- setPriority(int newPriority) 设置线程的优先级
- getPriority() 获取线程的优先级
- 最低优先级为1,默认优先级为5,最高优先级为10,优先级高的抢占到cpu时间片的概率可能会高一些,而并不是说一定会抢到
- 静态方法
- static void yield() 让位方法 暂停当前正在执行的线程对象,并执行其他线程
- 注意: yield()方法并不是阻塞方法,它会让当前线程让位,把当前线程的状态从"运行状态"回到"就绪状态",把机会给其他线程使用,而且在回到"就绪状态"之后,还是有可能会抢到cpu时间片的
- 实例方法
//线程优先级 public static void main(String[] args) { System.out.println("最高优先级: " + Thread.MAX_PRIORITY); //输出: 10 System.out.println("最低优先级: " + Thread.MIN_PRIORITY); //输出: 1 System.out.println("默认优先级: " + Thread.NORM_PRIORITY); //输出: 5 //获取当前线程的优先级 Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "线程的默认优先级为: " + thread.getPriority()); Thread t = new Thread(new MyRunnable5()); t.setName("线程x"); //设置线程优先级 t.setPriority(10); t.start(); } class MyRunnable5 implements Runnable{ @Override public void run() { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "的默认优先级为: " + thread.getPriority()); } }
- 常见的线程调度模型有哪些
线程让位
public static void main(String[] args) { Thread t = new Thread(new MyRunnable6()); t.setName("线程x"); t.start(); for(int i = 1;i <= 100;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); } }
class MyRunnable6 implements Runnable{ @Override public void run() { for(int i = 1;i <= 100;i++){ //每隔10个数就让位一下 if(i % 10 == 0){ //让位给主线程 Thread.yield(); } System.out.println(Thread.currentThread().getName() + "--->" + i); } } } ```
线程合并
public static void main(String[] args) { System.out.println("main begin..."); Thread t = new Thread(new MyRunnable7()); t.setName("线程x"); t.start(); //合并线程 try { //将线程x合并到当前线程中,此时当前线程会受阻塞,直到线程x结束,才继续执行主线程 t.join(); } catch (InterruptedException e) { e.printStackTrace(); } //所以这行语句永远是最后输出 System.out.println("main over..."); } class MyRunnable7 implements Runnable{ @Override public void run() { for(int i = 0;i < 100;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); } } }
线程安全
关于多线程并发环境下,数据的安全问题显得极为重要,这才是我们要掌握的重点
我们编写的程序如果放到一个多线程的环境下运行,我们就需要关注这些数据在多线程并发的环境下是否是安全的
什么时候数据在多线程并发的环境下会存在安全问题
- 多线程并发
- 有共享的数据
- 共享数据有修改的行为
怎么解决线程安全问题呢?
- 使用线程同步机制
- 实际上就是线程不能并发,只能排队执行,虽然线程并发效率高,但是存在数据安全问题
同步编程模型
线程A和线程B,各自执行各自的,A不关心B,B也不关心A,谁也不需要等谁,其实就是多线程并发
异步编程模型
线程A和线程B,在线程A执行的时候,必须等待线程B执行结束,或者说线程B执行的时候,必须等待线程A执行结束,两个线程之间有等待关系,这叫排队执行,效率较低
/* 模拟两个线程同时对一个账户进行取款 */ //如果不使用线程同步机制,多线程对同一账户进行取款,会出现线程安全问题 //账户类 public class Account { private String no; private double balance; public Account() { } public Account(String no, double balance) { this.no = no; this.balance = balance; } public String getNo() { return no; } public void setNo(String no) { this.no = no; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } //取款 public void withdraw(double money){ //取款前的余额 double before = this.getBalance(); //取款后的余额 double after = before - money; //如果在这里模拟一下网络延迟,100%会出现问题 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //更新余额 this.setBalance(after); }
@Override public String toString() { return "Account{" + "no='" + no + '\'' + ", balance=" + balance + '}'; } } public class AccountThread extends Thread{ //两个线程必须共享同一个账户对象,所以需要将账户对象定义为属性 private Account account; //通过构造方法传递账户对象 public AccountThread(Account account){ this.account = account; } @Override public void run() { //在这里进行取款操作 double money = 5000; //取款 account.withdraw(money); System.out.println(Thread.currentThread().getName() + "对" + account.getNo() + "取款成功,余额为: " + account.getBalance()); } } public static void main(String[] args) { //创建账户对象 Account account = new Account("001",10000); //创建两个线程,两个线程共享一个账户对象 AccountThread t1 = new AccountThread(account); AccountThread t2 = new AccountThread(account); t1.setName("t1"); t2.setName("t2"); //启动线程进行取款 t1.start(); t2.start(); } //然后我们使用线程同步机制,就可以解决线程安全问题 //其他代码不变,只需要改变withdraw()方法即可 //取款 public void withdraw(double money){ /* 以下几行代码必须是线程排队的,不能并发,一个线程把这里的代码全部执行完之后,另一个线程才能进来 线程同步机制的语法: synchronized (){ //线程同步代码块 } 注意: synchronized(),小括号里面传的这个数据是非常关键的 这个数据必须是多线程共享的数据,才能达到多线程排队 这里线程t1和线程t2共享的数据是账户对象,所以将this传进去 这里也不一定每次都是this,只要是多线程共享的那个对象就行 */ synchronized (this){ double before = this.getBalance(); double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); } } ```
synchronized关键字
/* 1. 在Java中,任何一个对象都有一把锁,其实这把锁就是一个标记 2. 以下代码的执行原理 1. 假设t1和t2线程并发,开始执行以下代码的时候,肯定会有一个先一个后 2. 假设t1先执行了,遇到了synchronized,这个时候t1就会自动去找()中的参数"这个共享对象的锁",找到之后,占有这把锁,然后执行同步代码块中的程序,在程序执行的过程中一直都是占有这把锁的,直到同步代码块执行结束,才会释放掉这把锁 3. 假设此时t1已经占有了这把锁,然后t2也遇到了synchronized,也会去占有"这个共享对象的锁",但是这把锁此时被t1占用了,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束,t1才会归还这把锁,此时t2就会拿到这把锁,然后t2会占有这把锁,进入同步代码块执行程序 4. 这样就达到了线程排队执行 5. 注意: 这个共享对象一定要选好,一定是需要排队执行的这些线程对象所共享的某个对象 6. 同步代码块越小,效率越高 7. */ //synchronized (this){ synchronized ("abc"){ //"abc"在字符串常量池当中,只有一份,是共享对象 double before = this.getBalance(); double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); } //这种方式也是可以的 @Override public void run() { //在这里进行取款操作 double money = 5000; //取款 synchronized (account){ //这样也是可以的,只不过扩大了同步范围,效率变得更低 account.withdraw(money); } System.out.println(Thread.currentThread().getName() + "对" + account.getNo() + "取款成功,余额为: " + account.getBalance()); }
Java中的三大变量
- 实例变量:在堆中
- 静态变量:在方法区中
- 局部变量:在栈中
- 注意:局部变量永远都不会存在线程安全问题,因为局部变量不共享,局部变量存储在栈中,一个线程一个栈,所以永远不会共享。常量也不会存在线程安全问题,因为常量一旦定义便无法修改
- 实例变量在堆中,堆只有一个,静态变量在方法区中,方法区也只有一个,堆和方法区都是线程共享的,所以可能会存在线程安全问题
synchronized进一步了解
//synchronized出现在实例方法上 /* 在实例方法是使用synchronized 注意: synchronized出现在实例方法上,一定锁的是this,不能是其他的对象了,所以这种方式不灵活 缺点: synchronized一旦出现在实例方法上,意味着整个方法体都需要同步,可能会扩大同步范围,导致程序的执行效率降低,所以不常用 优点: 代码写的少 如果共享的对象就是this,而且需要同步的代码块就是整个方法体,建议使用这种方式 */ public synchronized void withdraw(double money){ double before = this.getBalance(); double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); }
/* 总结: synchronized有三种写法 1. 同步代码块: 比较灵活 synchronized(线程共享对象){ 同步代码块 } 2. 在实例方法上使用synchronized 此时的共享对象一定是this,并且整个方法体就是同步代码块 3. 在静态方法上使用synchronized 表示此时找的是类锁 注意: 类锁永远只有1把,即使创建了100个对象,就会有100把对象锁,1把类锁 */ ```
死锁
/* 死锁代码一定要会写,只有会写,才能在开发中避免出现这种错误 重点: synchronized最好不要嵌套使用,否则容易出现死锁问题 */ public static void main(String[] args) { Object o1 = new Object(); Object o2 = new Object(); //创建两个线程,共享o1和o2 Thread1 t1 = new Thread1(o1,o2); Thread2 t2 = new Thread2(o1,o2); t1.start(); t2.start(); } class Thread1 extends Thread{ Object o1; Object o2; public Thread1(Object o1,Object o2){ this.o1 = o1; this.o2 = o2; } @Override public void run() { //先锁住o1,再锁住o2 synchronized (o1){ try { //休眠1秒,必会出现死锁 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2){ } } } } class Thread2 extends Thread{ Object o1; Object o2; public Thread2(Object o1,Object o2){ this.o1 = o1; this.o2 = o2; } @Override public void run() { //先锁住o2,再锁住o1 synchronized (o2){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1){ } } } }
在以后的开发中,我们应该怎么解决线程安全问题
方案一
尽量使用"局部变量"来代替"实例变量"和"静态变量"
方案二
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不会共享了,就不会存在数 据安全文通
方案三
如果不能使用局部变量,对象也不能够创建多个,这个时候就只能选择使用synchronized线程同步机制
守护线程
/* 1. Java语言中线程分为两大类 1. 用户线程 2. 守护线程(后台线程) 其中具有代表性的就是垃圾回收线程(守护线程) 2. 守护线程的特点 一般守护线程就是一个死循环,所有的用户线程只要结束,守护线程就会自动结束 3. 注意: 主线程main就是一个用户线程 4. 守护线程主要用在什么地方呢 例如: 每天00:00的时候让系统数据自动进行备份 这个时候我们可能需要定义一个定时器,将定时器定义为守护线程,一直在那里守着,每到00:00的时候就进行自动备份,如果所有的用户线程都结束了,那么定时器就会自动退出,没必要再进行数据备份了 */ public static void main(String[] args) { CopyDataThread t = new CopyDataThread(); t.setName("备份数据的线程"); //启动之前将t设置为守护线程 t.setDaemon(true); t.start(); //主线程,一旦用户线程结束,守护线程便会自动结束 //即使是死循环,但是由于该线程是守护者,当用户线程结束后,守护线程必须也要结束 for(int i = 0;i < 10;i++){ System.out.println(Thread.currentThread().getName() + "--->" + i); try { //睡眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } class CopyDataThread extends Thread{ @Override public void run() { int i = 0; //死循环 while(true){ System.out.println(Thread.currentThread().getName() + "--->" + i++); try { //睡眠1秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
定时器
/* 1. 定时器的作用 间隔特定的时间,执行特定的程序 2. 实现方式 1. 可以使用sleep()方法进行睡眠,设置睡眠时间,时间点一到,就起来执行任务,这是最原始的定时器 2. Java已经帮我们写好了一个定时器java.util.Timer,我们可以直接拿来用,但是这种方式用的也比较少,我们只需要了解即可,在开发中,我们都是使用框架来实现定时任务的 */ public static void main(String[] args) throws Exception{ //创建定时器对象 Timer timer = new Timer(); //设置守护线程的方式,参数为是否是守护线程 //Timer timer = new Timer(true); //指定定时任务 //timer.schedule(定时任务,第一次执行时间,间隔多久执行一次) SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date firstTime = sdf.parse("2021-01-10 10:15:00"); timer.schedule(new LogTimerTask(),firstTime,1000 * 10); } //编写一个定时器任务类,假设这是一个记录日志的定时任务 class LogTimerTask extends TimerTask{ @Override public void run() { //这里可以编写具体的定时任务 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String format = sdf.format(new Date()); System.out.println(format + "这个时间完成了一次数据备份"); } }
实现线程的第三种方式
/* 1. 实现线程的第三种方式: 实现Callable接口(JDK8的新特性) 2. 这种方式实现的线程,是可以获取到线程返回值的 3. 前面的那两种方式是无法获取线程返回值的,因为run()方法的返回值是void 4. 这种方式的优缺点 优点: 可以获取线程的执行结果 缺点: 在获取线程的执行结果的时候,当前线程会受阻塞,效率较低 */ import java.util.concurrent.FutureTask; //java.util.concurrent这个包属于java的并发包,***DK中是没有这个包的 public static void main(String[] args) throws ExecutionException, InterruptedException { //创建一个"未来任务类"对象 //构造方法中的参数非常重要,需要传一个Callable接口实现类的对象 FutureTask task = new FutureTask(new Callable() { //这个call()方法相当于run()方法,只不过call()方法有返回值 @Override public Object call() throws Exception { System.out.println("call begin..."); Thread.sleep(5000); System.out.println("call over"); return new Object(); } }); //创建线程对象 Thread t = new Thread(task); //启动线程 t.start(); //获取t线程的返回值 Object o = task.get(); System.out.println("执行结果为: " + o); //注意: 下面这行程序要想执行,必须等待get()方法结束 //因为get()方法是为了拿另一个线程的执行结果,而另一个线程的执行是需要一定时间的,所以这里会受阻塞等待 System.out.println("hello world"); }
//源码分析 public class FutureTask<V> implements RunnableFuture<V> //可以使用泛型 private Callable<V> callable; //构造方法 public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } //如果传Runnable接口的实现来对象,是无法获取到返回值的 public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable } //Callable接口 public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; } ```
wait()方法和notify()方法
/* 1. wait方法和notify方法不是线程对象的方法,而是Java中任何一个对象都有的方法,因为这两个方法是Object类自带的 2. wait方法的作用 Object o = new Object(); o.wait(); 让正在o对象上活动的当前线程进入等待状态,这里的等待是无限期等待,直到被唤醒为止 3. notify方法的作用 Object o = new Object(); o.notify(); 唤醒o对象处于等待的线程 o.notifyAll(); 唤醒o对象上处于等待的所有线程 4. 重点 1. wait和notify方法都是建立在synchronized线程同步的基础之上的 2. o.wait()方***让正在o对象上活动的当前线程进入等待状态,并且会释放之前占有的o对象的那把锁 3. o.notify()方法只会唤醒,并不会释放之前占有的o对象的那把锁 */
生产者和消费者模式
/* 1. 使用wait方法和notify方法实现"生产者和消费者模式" 2. 生产者和消费者模式的概念 生产者线程: 负责生产 消费者线程: 负责消费 生产线程和消费现场之间要达到均衡状态 这是一种特殊的业务需求,在这种特殊的情况下需要用到wait方法和notify方法 */ public static void main(String[] args) { /* 模拟一个需求: 1. 仓库我们采用List集合 2. List集合中只能存储一个元素,1个元素就表示仓库满了 3. 如果List集合中元素个数为0,就表示仓库空了 4. 生产一个消费一个 */ //创建仓库对象 List list = new ArrayList(); //创建两个线程 Thread t1 = new Thread(new Producer(list)); Thread t2 = new Thread(new Consumer(list)); t1.setName("生产者线程"); t2.setName("消费者线程"); t1.start(); t2.start(); } //生产线程 class Producer implements Runnable{ //共享同一个仓库 private List list; public Producer(List list){ this.list = list; } @Override public void run() { //一直生产 while(true){ //给仓库对象加锁 synchronized (list){ if(list.size() > 0){ //说明此时仓库中有1个元素 try { list.wait(); //当前线程进入等待,并释放之前占有的list集合的锁 } catch (InterruptedException e) { e.printStackTrace(); } } //如果程序执行到这,就代表此时仓库中没有元素,可以生产 Object o = new Object(); list.add(o); System.out.println(Thread.currentThread().getName() + "生产了" + o + "对象"); //此时就可以唤醒消费者进行消费 list.notify(); } } } } //消费线程 class Consumer implements Runnable{ //共享同一个仓库 private List list; public Consumer(List list){ this.list = list; } @Override public void run() { //一直消费 while(true){ //给仓库对象加锁 synchronized (list){ if(list.size() == 0){ //说明此时仓库为空 try { list.wait(); //当前线程进入等待,并释放之前占有的list集合的锁 } catch (InterruptedException e) { e.printStackTrace(); } } //如果程序执行到这,就代表此时仓库中有元素,可以进行消费 Object o = list.remove(0); System.out.println(Thread.currentThread().getName() + "消费了" + o + "对象"); //此时就可以唤醒生产者进行生产 list.notify(); } } } }
反射机制
反射机制的作用
通过反射机制,我们可以操作类的字节码文件
反射机制相关的重要的类
- java.lang.Class:代表整个字节码文件
- java.lang.reflect.Method:代表字节码中的方法字节码
- java.lang.reflect.Constructor:代表字节码中的构造方法字节码
- java.lang.reflect.Field:代表字节码中的属性字节码
获取Class的三种方式
/* 1. 要想操作一个类的字节码,首先就需要获取到这个类的字节码 2. 有三种获取方式 1. Class.forName() 1. 静态方法 2. 方法的参数是一个字符串 3. 字符串需要一个完整的类名,包名不能省 2. Java中任何一个对象都有一个方法: Class c = 对象.getClass() 3. Java中任何一种类型,包括基本数据类型,都有.class属性: Class c = 任何类型.class */ //方式一 public static void main(String[] args) { try { //获取Class Class c1 = Class.forName("java.lang.String"); Class c2 = Class.forName("java.util.Date"); Class c3 = Class.forName("java.lang.Integer"); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
//方式二
public static void main(String[] args) {
String s = "xinxin";
try {
Class c1 = Class.forName("java.lang.String");
Class c2 = s.getClass();
//"=="比较的是内存地址
System.out.println(c1 == c2); //输出: true
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//方式三
public static void main(String[] args) {
try {
Class c1 = String.class;
Class c2 = Class.forName("java.lang.String");
System.out.println(c1 == c2); //输出: true
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
4. 通过反射实例化对象 ```java /* 通过反射机制,先获取Class,然后通过Class来实例化对象 */ public static void main(String[] args) { try { //通过反射机制,先获取Class,然后通过Class来实例化对象 Class c = Class.forName("com.siki.reflect.User"); //c代表User类型 //实例化对象 //newInstance()方***调用User这个类的无参构造方法,完成对象的创建 //重点: newInstance()方法调用的是无参构造,如果没有无参构造会报错,所以必须保证无参构造是存在的 Object o = c.newInstance(); System.out.println(o); } catch (Exception e) { e.printStackTrace(); } } class User{ public User(){ System.out.println("无参构造方法..."); } public User(int i ){ System.out.println("有参构造方法..."); } }
通过读取属性文件实例化对象
/* 1. 相比于我们之前学的创建对象的方式,通过反射机制创建对象可能比较麻烦,但是更加具有灵活性 2. 我们可以通过读取属性文件来实例化对象,这里就可以看出它的灵活性 3. Java代码写一遍,我们可以不改变java代码,只改变属性文件的内容,就可以做到不同对象的实例化,非常灵活,符合OOP原则(对扩展开放,对修改关闭) */ public static void main(String[] args) throws Exception { //通过IO流来读取配置文件中的信息 FileReader reader = new FileReader("classInfo.properties"); //创建属性类对象 Properties p = new Properties(); //加载 p.load(reader); //获取数据 String s = p.getProperty("className"); System.out.println(s); //通过反射机制实例化对象 Class c = Class.forName(s); //创建对象 Object o = c.newInstance(); System.out.println(o); //关闭流 reader.close(); } class User2{ public User2(){ System.out.println("无参构造方法..."); } public User2(int i ){ System.out.println("有参构造方法..."); } } //属性文件 classInfo.properties #className=com.siki.reflect.User2 className=java.util.Date
静态代码块执行
/* 1. Class.forName()这个方法的执行,必然会导致类加载,也就会执行静态代码块 2. 所以如果你只想执行静态代码块,不希望执行其他的代码的话,使用Class.forName()方法即可 */ public static void main(String[] args) { try { Class.forName("com.siki.reflect.MyClass"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } class MyClass{ //类加载的时候执行静态代码块,而且只执行一次 static { System.out.println("MyClass的静态代码块执行了..."); } }
获取类路径下的文件的绝对路径
/* 以下这种方式的路径缺点: 移植性差,在IDEA当中默认的当前路径是project的根,如果换到了其他地方,可能当前路径就不再是project的根了,此时这个路径将会无效 */ FileReader reader = new FileReader("classInfo.properties"); /* 下面这种方式是一种比较通用的方式,即使代码换位置了,还是能够执行 注意: 前提是这个文件必须在类路径下(也就说必须放在src下,否则获取不到) Thread.currentThread() 获取当前线程 Thread.currentThread().getContextClassLoader() 获取当前线程的类加载器对象 getResource() 这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下获取资源 */ String path = Thread.currentThread().getContextClassLoader() .getResource("classInfo2.properties").getPath(); String path2 = Thread.currentThread().getContextClassLoader() .getResource("com/siki/bean/db.properties").getPath();
以流的方式直接返回
//方式一 public static void main(String[] args) throws IOException { //获取文件的绝对路径 String path = Thread.currentThread().getContextClassLoader() .getResource("classInfo2.properties").getPath(); System.out.println(path); //创建流对象 FileReader reader = new FileReader(path); //创建集合对象 Properties p = new Properties(); //加载 p.load(reader); System.out.println(p.getProperty("className")); //关闭流 reader.close(); } //方式二 public static void main(String[] args) throws IOException { //直接以流的形式返回 InputStream reader = Thread.currentThread().getContextClassLoader(). getResourceAsStream("classInfo2.properties"); //创建集合对象 Properties p = new Properties(); //加载 p.load(reader); System.out.println(p.getProperty("className")); //关闭流 reader.close(); }
资源绑定器
/* 1. java.util包下提供了一个资源绑定器,便于我们获取属性配置文件中的内容 2. 注意以下几点 1. 使用这种方式的时候,属性配置文件xxx.properties必须放到类路径src下 2. 资源绑定器只能绑定properties文件,其他文件不行 3. 写路径的时候,只需要写xxx就可以了,路径后面的扩展名.properties不能写 */ public static void main(String[] args) { //有了这种方式,就不需要使用IO + Properties类的方式了 ResourceBundle bundle = ResourceBundle.getBundle("classInfo2"); String className = bundle.getString("className"); System.out.println(className); }
类加载器概述
专门负责加载类的命令/工具 ClassLoader
JDK中自带了3个类加载器
- 启动类加载器
- 扩展类加载器
- 应用类加载器
/* 1. 以下代码在开始执行之前,会将所有需要的类都加载到JVM当中 2. 通过类加载器加载,首先会加载String.class文件,如果找到就加载 1. 首先通过"启动类加载器"进行加载 注意: 启动类加载器专门加载: E:\JAVA\jdk1.8\jre\lib\rt.jar rt.jar中都是JDK最核心的类库 2. 如果通过"启动类加载器"加载不到的时候,就会通过"扩展类加载器"进行加载 注意: 扩展类加载器专门加载: E:\JAVA\jdk1.8\jre\lib\ext\*.jar 3. 如果"扩展类加载器"没有加载到,那么就会通过"应用类加载器"进行加载 注意: 应用类加载器专门加载: 环境变量中classpath中的类 3. Java中为了保存类加载的安全,使用了"双亲委派机制" 1. 优先从"启动类加载器"中加载,这个称为"父加载器",如果"父加载器"无法加载到,再从"扩展类加载器"当中加载,这个称为"母加载器",如果都加载不到,就会考虑从应用类加载器当中加载,直到加载到为止 */ String s = "xinxin";
获取Field
public static void main(String[] args) throws Exception { //先获取整个类 Class c = Class.forName("com.siki.reflect.Student"); //获取类的名称 System.out.println("完整类名为: " + c.getName()); //输出: com.siki.reflect.Student System.out.println("简单类名为: " + c.getSimpleName()); //输出: Student //获取所有public修饰的Field Field[] fields = c.getFields(); System.out.println("public修饰的属性个数为: " + fields.length); //输出: 1 System.out.println(fields[0]); //输出: public int com.siki.reflect.Student.age System.out.println(fields[0].getName()); //输出: age //获取所有的Field Field[] declaredFields = c.getDeclaredFields(); System.out.println("所有属性个数为: " + declaredFields.length); //输出: 4 for(Field f : declaredFields){ //获取属性的修饰符列表 int modifiers = f.getModifiers(); //返回值是一个int,不同的数字代表不同的修饰符类型 //System.out.println(modifiers); //我们可以将int转为String String s = Modifier.toString(modifiers); System.out.println(s); //获取属性的类型 Class type = f.getType(); System.out.println(type.getSimpleName()); //获取属性的名称 System.out.println(f.getName()); } } class Student{ private int no; protected String name; public int age; boolean sex; }
反编译Field
//通过反射机制,我们可以在知道类名的情况下反编译出该类的所有属性 public static void main(String[] args) throws Exception { //获取类 Class c = Class.forName("com.siki.reflect.Student"); //创建这个是为了拼接字符串 StringBuilder s = new StringBuilder(); s.append(Modifier.toString(c.getModifiers()) + "class " + c.getSimpleName() + " {\n"); //获取所有的属性 Field[] fields = c.getDeclaredFields(); for(Field f : fields){ s.append("\t"); s.append(Modifier.toString(f.getModifiers())); s.append(" "); s.append(f.getType().getSimpleName()); s.append(" "); s.append(f.getName()); s.append(";\n"); } s.append("}"); System.out.println(s); }
通过反射机制访问一个java对象的属性
/* 通过反射机制获取对象的属性 给属性赋值set 获取属性的值get */ public static void main(String[] args) throws Exception { //原始的属性赋值 Student s = new Student(); int i = s.age = 10; System.out.println(i); //输出: 10 //获取类 Class c = Class.forName("com.siki.reflect.Student"); //创建对象 Object o = c.newInstance(); //获取某个属性 Field field = c.getDeclaredField("age"); //给某个对象的某个属性赋值 field.set(o,20); //读取属性的值 System.out.println(field.get(o)); //输出: 20 //怎么访问私有的属性呢 Field no = c.getDeclaredField("no"); //打破封装(这个也是反射机制的缺点) no.setAccessible(true); //赋值 no.set(o,1314); System.out.println(no.get(o)); }
可变长度参数
/* 1. 可变长度参数要求参数的个数是: 1-N个 2. 可变长度参数在参数列表中必须在最后一个位置上,而且可变长度参数只能有1个 3. 可变长度参数可以当作一个数组来看待 */ public static void main(String[] args) { m1(); m1(10); m1(20,30); String[] strings = {"我","爱","昕","昕"}; m2(10,strings); } public static void m1(int...args){ System.out.println("m1方法执行了..."); } public static void m2(int n,String...args){ for(int i = 0;i < args.length;i++){ System.out.println(args[i]); } }
反射Method
public static void main(String[] args) throws Exception { //获取类 Class userService = Class.forName("com.siki.reflect.UserService"); //获取所有的方法,包括私有的 Method[] methods = userService.getDeclaredMethods(); System.out.println(methods.length); //输出: 2 for(Method m : methods){ //获取方法的返回值类型 Class type = m.getReturnType(); System.out.println(type.getSimpleName()); //获取方法名 System.out.println(m.getName()); //获取方法的参数列表(参数可能会有多个,所以返回值是Class数组) Class[] parameterTypes = m.getParameterTypes(); for(Class c : parameterTypes){ System.out.println(c.getSimpleName()); //返回值是参数的数据类型 } } } class UserService{ public boolean login(String username,String password){ if("admin".equals(username) && "123456".equals(password)){ return true; } return false; } public void logOut(){ System.out.println("已经安全退出系统..."); } }
反射机制调用方法
public static void main(String[] args) throws Exception { //不使用反射机制,调用方法 UserService service = new UserService(); boolean b = service.login("admin", "123456"); System.out.println(b ? "登陆成功" : "登录失败"); service.logOut(); //使用反射机制调用方法 Class c = Class.forName("com.siki.reflect.UserService"); //创建对象 Object o = c.newInstance(); //获取要调用的方法 Method m = c.getDeclaredMethod("login", String.class, String.class); //调用方法 Object o1 = m.invoke(o, "admin", "123456"); System.out.println(o1); //输出: true }
反射Constructor
public static void main(String[] args) throws Exception { //获取类 Class vipClass = Class.forName("com.siki.reflect.Vip"); StringBuilder s = new StringBuilder(); s.append("public class Vip{\n"); //拼接构造方法 Constructor[] constructors = vipClass.getConstructors(); for(Constructor c : constructors){ s.append("\t"); s.append(Modifier.toString(c.getModifiers())); s.append(" "); s.append(vipClass.getSimpleName()); s.append("("); //拼接参数 Class[] parameterTypes = c.getParameterTypes(); for(Class parameter : parameterTypes){ s.append(parameter.getSimpleName()); s.append(","); } //删除最参数最后面的那一个逗号 //无参构造是没有最后那个逗号的,所以这里需要判断一下 if(parameterTypes.length > 0) { s.deleteCharAt(s.length() - 1); } s.append("){}\n"); } s.append("}"); System.out.println(s); } class Vip{ private int no; private String name; private String password; public Vip() { } public Vip(int no) { this.no = no; } public Vip(int no, String name) { this.no = no; this.name = name; } public Vip(int no, String name, String password) { this.no = no; this.name = name; this.password = password; } @Override public String toString() { return "Vip{" + "no=" + no + ", name='" + name + '\'' + ", password='" + password + '\'' + '}'; } }
反射机制调用构造方法
public static void main(String[] args) throws Exception { //不使用反射机制创建对象 Vip v1 = new Vip(); Vip v2 = new Vip(1,"昕昕","1314520"); //使用反射机制调用构造方法创建对象 Class c = Class.forName("com.siki.reflect.Vip"); //调用无参构造 Object o = c.newInstance(); System.out.println(o); //调用有参构造 Constructor constructor = c.getConstructor(int.class, String.class, String.class); Object o1 = constructor.newInstance(1, "昕昕", "1314520"); System.out.println(o1); }
获取父类和父接口
public static void main(String[] args) throws Exception{ //获取String类的父类和实现的接口 Class c = Class.forName("java.lang.String"); //获取String的父类 Class superclass = c.getSuperclass(); System.out.println(superclass.getName()); //获取String实现的接口 Class[] interfaces = c.getInterfaces(); for(Class i : interfaces){ System.out.println(i.getName()); } }
注解
注解:是一种引用数据类型,编译完之后也是xxx.class文件
自定义注解的语法格式
/* [修饰符列表] @interface 注解类型名{ } 注解的使用方式 1. 注解使用时的语法是: @注解类型名 2. 注解可以出现在类上,也可以出现在属性上、方法上、变量上等...... 3. 注解还可以出现在注解类型上面 */ @MyAnnotation public class AnnotationTest01 { @MyAnnotation private int no; private String name; @MyAnnotation public AnnotationTest01(){ } public static void main(String[] args) { } } //自定义注解 @interface MyAnnotation{ }
Override注解
/* 1. @Override这个注解只能够注解方法 2. @Override注解只是给编译器看的,和运行阶段没有关系 3. 只要是方法上面有这个注解,编译器就会检查这个方法是否是重写父类的方法,如果不是,编译器就会报错 */
元注解
/* 1. 什么是元注解 用来标注"注解类型"的注解,称为元注解 2. 常见的元注解 @Target 该注解用来标注"被标注的注解"可以出现在哪些位置上 @Target(ElementType.METHOD) 表示"被标注的注解"只能出现在方法上面 @Retention 该注解用来标注"被标注的注解"最终保存在哪里 @Retention(RetentionPolicy.SOURCE) 表示"被标注的注解"最终只能保存在java源文件中 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
Deprecated
/* 1. @Deprecated注解标注的元素表示已过时 2. 这个注解主要是为了向其他程序员传递一个信息,该类或者方法已过时,有更好的解决方案存在 */ public static void main(String[] args) { MyDeprecated d = new MyDeprecated(); MyDeprecated.m1(); d.m2(); } @Deprecated class MyDeprecated{ @Deprecated public static void m1(){ System.out.println("m1方法已过时"); } @Deprecated public void m2(){ System.out.println("m2方法已过时"); } } //源码 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
注解中定义属性
//如果注解中定义了属性,我们使用该注解的时候,就必须为属性进行赋值 (格式: 属性名=属性值) 否则会报错,除非该属性指定了默认值 @Annotation(name = "xinxin",value = "520") public static void m(){ } @interface Annotation{ //我们通常可以在注解当中定义属性 //注意: 这个是属性,不是方法 String name(); String value(); //当我们不想为属性赋值时,我们可以为属性指定默认值 String password() default "1314520"; }
属性名是value时,可以省略
@Annotation2(value = "520") public static void doSome(){ } //如果一个注解只有一个属性而且属性名是value,在使用的时候,可以不写,如果有两个及以上的属性名,那就不能不写 @Annotation2("520") public static void doOther(){ } @interface Annotation2{ String value(); }
属性是一个数组
/* 注解中的属性可以是哪一种类型 byte short int long float double char boolean String Class 枚举类型 以及每一种类型的数组形式,其他类型就不行了 */ //数组使用大括号 @Annotation3(name = "xinxin",password = {"13","14","520"}) public static void doSome(){ } //如果数组中只有一个元素,大括号可以不用写 @Annotation3(name = "xinxin",password = "1314520") public static void doOther(){ } @interface Annotation3{ String name(); String[] password(); }
通过反射获取注解对象及注解中的属性
public static void main(String[] args) throws Exception { //获取这个类 Class c = Class.forName("com.siki.annotation.AnnotationTest05"); //判断这个类上面是否有@Annotation4注解 boolean b = c.isAnnotationPresent(Annotation4.class); System.out.println(b); //如果有该注解,就可以获取 if(b){ //先获取到该注解 Annotation4 annotation = (Annotation4) c.getAnnotation(Annotation4.class); //再获取注解里面的属性 String value = annotation.value(); System.out.println(value); } } @Target({ElementType.TYPE,ElementType.METHOD}) //表示只允许该注解标注在类和方法上 @Retention(RetentionPolicy.RUNTIME) //表示该注解可以被反射 @interface Annotation4{ String value() default "xinxin"; }
通过反射获取注解对象属性里面的值
public static void main(String[] args) throws Exception { //获取m方法上面的注解的相关信息 //先获取类 Class c = Class.forName("com.siki.annotation.AnnotationTest06"); //再获取m()方法 Method method = c.getDeclaredMethod("m"); //判断该方法是是否存在这个注解 if(method.isAnnotationPresent(Annotation5.class)){ Annotation5 annotation = method.getAnnotation(Annotation5.class); System.out.println(annotation.username()); System.out.println(annotation.password()); } } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface Annotation5{ String username(); String password(); }