JavaSE-进阶


JavaSE进阶

面向对象

Final

  • final 表示不可改变的含义

  • 采用 final 修饰的类不能被继承

  • 采用 final 修饰的方法不能被覆盖

  • 采用 final 修饰的变量不能被修改

  • final 修饰的变量必须显示初始化

  • 如果修饰的引用,那么这个引用只能指向一个对象,也就是说这个引用不能再次赋值, 但被指向的对象是可以修改的

  • 构造方法不能被 final 修饰

抽象类和接口

抽象类

抽象类定义:

1
2
3
[修饰符列表] abstract class 类名{
类体;
}
  • 抽象类无法实例化,无法创建对象,所以抽象类用来被子类继承。
  • final和abstract不能联合使用,两个关键字是对立的。
  • 抽象类的子类可以是抽象类,也可以是非抽象类。
  • 抽象类无法实例化,但有构造方法,供子类使用。
  • 抽象类中不一定有抽象方法,抽象方法必须出现在抽象类中

抽象方法定义:

1
public abstract void doSome();//没有方法体,以分号结尾。
  • 一个非抽象的类,继承抽象类,必须将抽象类中的抽象方法进行覆盖/重写/实现
  • 抽象方法不能有方法体
  • 抽象类降低接口实现类与接口之间的实现难度。

接口

接口是一种“引用数据类型”,是完全抽象的。

定义:

1
[修饰符列表] interface 接口名{}
  • 接口支持多继承。
  • 接口中只有常量+抽象方法。
  • 接口中所有的元素都是public修饰的
  • 接口中抽象方法的public abstract可以省略。
  • 接口中常量的public static final可以省略。

实现接口

本质:对接口的继承

1
2
3
4
class D implements A,B,C{//实现多个接口,类似于多继承
// 实现A接口的m1()
public void m1(){
}

一个非抽象的类,实现接口时,必须将接口中所有方法加以实现

一个类可以实现多个接口

extends和implements可以共存,extends在前,implements在后。

使用接口,写代码的时候,可以使用多态(父类型引用指向子类型对象)。

package 和 import

关于java语言中的package和import机制:

  • 为什么要使用package?

        package是java中包机制。包机制的作用是为了方便程序的管理。不同功能的类分别存放在不同的包下。
    
  • 用法

        package是一个关键字,后面加包名。例如:
    
1
package com.a.b.c;

注意:package语句只允许出现在java源代码的第一行。

  • 包名命名规范:

        包名命名规范:
              公司域名倒序 + 项目名 + 模块名 + 功能名
    
  • import什么时候使用?

            A类中使用B类时,A和B类都在同一个包下,不需要import;不在同一个包下。需要使用            import。
              java.lang.*;这个包下的类不需要。
    
  • import怎么用?

            import语句只能出现在package语句之下,class声明语句之上。
              import语句还可以采用星号的方式。
    

访问控制权限

类别:

private,public,protected,默认

修饰范围:

​ 属性(4个都能用)
​ 方法(4个都能用)
​ 类(public和默认能用,其它不行。)
​ 接口(public和默认能用,其它不行。)
​ …..

控制范围

​ private 只能在本类中访问
​ public 在任何位置都可以访问
​ 默认 只能在本类,以及同包下访问
​ protected 只能在本类、同包、子类中访问

Object类和内部类

Object类

在Object类中找方法
第一种:去源代码当中。(但是这种方式比较麻烦)
第二种:去查阅java的类库的帮助文档。

什么是API?
应用程序编程接口。(Application Program Interface)
整个JDK的类库就是一个javase的API。
每一个API都会配置一套API帮助文档。
SUN公司提前写好的这套类库就是API。(一般每一份API都对应一份API帮助文档。)

常用方法:

1
2
3
4
5
protected Object clone()  		 			  // 负责对象克隆
int hashCode() // 获取对象哈希值
boolean equals(Object obj) // 判断两个对象是否相等
String toString() // 将对象转换成字符串形式
protected void finalize() // 垃圾回收器负责调用的方法
toString()方法

​ 所有类的toString()方法是需要重写的。重写越简单越明了就好。

System.out.println(引用); 这里会自动调用“引用”的toString()方法。

String类是SUN写的,toString方法已经重写了。
equals()方法
  • 除基本数据类型外,对象之间比较应调用equals方法。因为Object中的equals方法比较两个对象的内存地址而非内容(默认使用==),所以大部分情况下equals方法需要重写。

  • String类自带重写后的equals方法。故判断两个字符串是否相等,要调用字符串对象的equals方法,最好不要使用==。

注意:注意检查所有类是否重写了equals方法。

内部类

定义:

在类的内部又定义了一个新的类。被称为内部类。

分类:

​ 静态内部类:类似于静态变量
​ 实例内部类:类似于实例变量
​ 局部内部类:类似于局部变量

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test01{
//该类在类的内部,所以称为内部类
//静态变量
static String country;
由于前面有static,所以称为“静态内部类”:
static class Inner1{
}

// 实例变量
int age;
没有static叫做实例内部类:
class Inner2{
}

// 方法
public void doSome(){
// 局部变量
int i = 100;
局部内部类:
class Inner3{
}
}
匿名内部类

匿名内部类是局部内部类的一种,因没有名字而得名。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Test01{
public static void main(String[] args){
使用匿名内部类
mm.mySum(new Compute(){
public int sum(int a, int b){
return a + b;
}
}, 200, 300);
未使用匿名内部类:
mm.mySum(new ComputeImpl(), 100, 200);
}
}

// 负责计算的接口
interface Compute{

// 抽象方法
int sum(int a, int b);
}

// Compute接口的实现类
class ComputeImpl implements Compute{
// 对方法的实现
public int sum(int a, int b){
return a + b;
}
}
缺点:

使用内部类编写的代码,可读性很差。能不用尽量不用。

数组

性质

  • Java语言中的数组是一种引用数据类型。不属于基本数据类型。数组的父类是Object
  • 数组当中可以存储“基本数据类型”的数据,也可以存储“引用数据类型”的数据。
  • 数组是引用类型,所以数组存储在中。
  • 数组中存储“java对象”时,实际上存储的是对象的“引用(内存地址)”,不能直接存储java对象。
  • 数组一旦创建,长度不可变。
  • 数组的分类:一维数组、二维数组、三维数组、多维数组…。
  • 所有的数组对象都有length属性(java自带的),用来获取数组中元素的个数。
  • java中的数组要求数组中元素的类型统一。比如Person类型数组只能存储Person类型。
  • 数组在内存方面存储的时候,数组中的元素内存地址(存储的每一个元素都是有规则的挨着排列的)是连续的。内存地址连续。
  • 数组中首元素的内存地址作为整个数组对象的内存地址。

缺点:

  • 由于为了保证数组中每个元素的内存地址连续,所以在数组上随机删除或者增加元素的时候,

    **效率较低**,因为随机增删元素会涉及到后面元素统一向前或者向后位移的操作。
    
  • 数组不能存储大数据量,因为很难在内存空间上找到一块特别大的连续的内存空间。

    数组的内存结构

声明/定义一维数组

​ 语法格式:

1
2
3
4
5
int[] array1;
double[] array2;
boolean[] array3;
String[] array4;
Object[] array5;

初始化一维数组

​ 包括两种方式:静态初始化一维数组,动态初始化一维数组。

静态初始化语法格式:

1
int[] array = {100, 2100, 300, 55};

动态初始化语法格式:

1
2
3
int[] array = new int[5]; // 这里的5表示数组的元素个数。
// 初始化一个5个长度的int类型数组,每个元素默认值0
String[] names = new String[6]; // 初始化6个长度的String类型数组,每个元素默认值null。

main方法的String数组

main方法上面的String[] args数组主要用来接收用户输入参数。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.javase.array;
/*
模拟一个系统,假设这个系统要使用,必须输入用户名和密码。
*/
public class ArrayTest06 {
// 用户名和密码输入到String[] args数组当中。
public static void main(String[] args) {
if(args.length != 2){
System.out.println("使用该系统时请输入程序参数,参数中包括用户名和密码信息,例如:zhangsan 123");
return;
}
// 程序执行到此处说明用户确实提供了用户名和密码。
// 接下来判断用户名和密码是否正确。
// 取出用户名
String username = args[0];
// 取出密码
String password = args[1];
// 假设用户名是admin,密码是123的时候表示登录成功。其它一律失败。
// 判断两个字符串是否相等,需要使用equals方法。
//if(username.equals("admin") && password.equals("123")){
// 采用以下编码风格,即使username和password都是null,也不会出现空指针异常。
if("admin".equals(username) && "123".equals(password)){
System.out.println("登录成功,欢迎[" + username + "]回来");
System.out.println("您可以继续使用该系统....");
}else{
System.out.println("验证失败,用户名不存在或者密码错误!");
}
}
}

数组拷贝

调用方法:

1
System.arraycopy(5个参数);

数组扩容

java中对数组的扩容:

​ 先新建一个大容量的数组,然后将小容量数组中的数据拷贝到大数组当中。数组扩容效率较低。故应尽可能少的进行数组的拷贝,提高效率。

二维数组

二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组。多维数组同理。

1
a[二维数组中的一维数组的下标][一维数组的下标]

取出二维数组中的一维数组。

1
int[] b = a[0];

静态初始化

1
int[][] array = {{1,1,1},{2,3,4,5},{0,0,0,0},{2,3,4,5},{2,3,4,5},{2,3,4,5},{2,3,4,5}};

动态初始化

1
int[][] array = new int[3][4];

Arrays工具类

包含关于排序和查找的内容

使用例:

1
2
3
4
// 排序
Arrays.sort(arr);
// 二分法查找(建立在排序基础之上。)
int index = Arrays.binarySearch(arr, 5);

常用类

String字符串

String表示字符串类型,属于引用数据类型。在JDK当中双引号括起来的字符串例如:”abc” “def”都直接存储在“方法区”的“字符串常量池”当中。

分析以下程序,一共创建了几个对象:

1
2
3
4
5
6
7
8
9
10
11
12
public class StringTest03 {
public static void main(String[] args) {
/*
一共3个对象:
方法区字符串常量池中有1个:"hello"
堆内存当中有两个String对象。
一共3个。
*/
String s1 = new String("hello");
String s2 = new String("hello");
}
}

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为对象已经存在了)
String s2 = "hello";
// == 双等号比较变量中保存的内存地址
System.out.println(s1 == s2); // true
String x = new String("xyz");
String y = new String("xyz");
System.out.println(x == y); //false
// 通过这个案例的学习,我们知道了,字符串对象之间的比较不能使用“==”
// String类已经重写了equals方法,以下的equals方法调用的是String重写之后的equals方法。
System.out.println(x.equals(y)); // true
}
}

String相关面试题

String常用方法

String类中的构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
第一个:String s = new String("");
String s6 = new String("helloworld!");
第二个:String s = ""; 最常用
String s1 = "hello world!";
第三个:String s = new String(char数组);
// 将char数组全部转换成字符串
char[] chars = {'我','是','中','国','人'};
String s4 = new String(chars);
System.out.println(s4);
第四个:String s = new String(char数组,起始下标,长度);
// 将char数组的一部分转换成字符串
String s5 = new String(chars, 2, 3);
第五个:String s = new String(byte数组);
byte[] bytes = {97, 98, 99}; // 97是a,98是b,99是c
String s2 = new String(bytes);
// 输出字符串对象的话,输出的不是对象的内存地址,而是字符串本身。
System.out.println(s2.toString()); //abc
System.out.println(s2); //abc
第六个:String s = new String(byte数组,起始下标,长度)
// 将byte数组中的一部分转换成字符串。
String s3 = new String(bytes, 1, 2);
System.out.println(s3); // bc
其他方法
  • (掌握).char charAt(int index)

    1
    2
    char c = "中国人".charAt(1); 
    System.out.println(c); // 国
  • (了解).int compareTo(String anotherString)

1
2
3
4
5
6
7
8
9
10
// 字符串之间比较大小需要使用compareTo方法。
int result = "abc".compareTo("abc");
System.out.println(result); //0(等于0) 前后一致 10 - 10 = 0

int result2 = "abcd".compareTo("abce");
System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1

int result3 = "abce".compareTo("abcd");
System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1

  • (掌握).boolean contains(CharSequence s)
1
2
3
// 判断前面的字符串中是否包含后面的子字符串。
System.out.println("HelloWorld.java".contains(".java")); // true
System.out.println("http://www.baidu.com".contains("https://")); // false
  • (掌握). boolean endsWith(String suffix)
1
2
3
4
// 判断当前字符串是否以某个子字符串结尾。
System.out.println("test.txt".endsWith(".java")); // false
System.out.println("test.txt".endsWith(".txt")); // true
System.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true
  • (掌握).boolean equals(Object anObject)
1
2
3
4
// 比较两个字符串必须使用equals方法,不能使用“==”
// equals只能看出相等不相等。
// compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
System.out.println("abc".equals("abc")); // true
  • (掌握).boolean equalsIgnoreCase(String anotherString)
1
2
// 判断两个字符串是否相等,并且同时忽略大小写。
System.out.println("ABc".equalsIgnoreCase("abC")); // true
  • (掌握).byte[] getBytes()
1
2
3
4
5
// 将字符串对象转换成字节数组
byte[] bytes = "abcdef".getBytes();
for(int i = 0; i < bytes.length; i++){
System.out.println(bytes[i]);
}
  • (掌握).int indexOf(String str)
1
2
// 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。
System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); //6
  • (掌握).boolean isEmpty()
1
2
3
4
// 判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。
//String s = "";
String s = "a";
System.out.println(s.isEmpty());
  • (掌握). int length()
1
2
3
// 面试题:判断数组长度和判断字符串长度不一样
// 判断数组长度是length属性,判断字符串长度是length()方法。
System.out.println("abc".length()); // 3
  • (掌握).int lastIndexOf(String str)
1
2
// 判断某个子字符串在当前字符串中最后一次出现的索引(下标)
System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22
  • (掌握). String replace(CharSequence target, CharSequence replacement)
1
2
3
4
5
6
7
// 替换。
// String的父接口就是:CharSequence
String newString = "http://www.baidu.com".replace("http://", "https://");
System.out.println(newString); //https://www.baidu.com
// 把以下字符串中的“=”替换成“:”
String newString2 = "name=zhangsan&password=123&age=20".replace("=", ":");
System.out.println(newString2); //name:zhangsan&password:123&age:20
  • (掌握).String[] split(String regex)
1
2
3
4
5
6
7
8
9
10
11
// 拆分字符串
String[] ymd = "1980-10-11".split("-"); //"1980-10-11"以"-"分隔符进行拆分。
for(int i = 0; i < ymd.length; i++){
System.out.println(ymd[i]);
}
String param = "name=zhangsan&password=123&age=20";
String[] params = param.split("&");
for(int i = 0; i <params.length; i++){
System.out.println(params[i]);
// 可以继续向下拆分,可以通过“=”拆分。
}
  • (掌握).boolean startsWith(String prefix)
1
2
3
// 判断某个字符串是否以某个子字符串开始。
System.out.println("http://www.baidu.com".startsWith("http")); // true
System.out.println("http://www.baidu.com".startsWith("https")); // false
  • (掌握)、 String substring(int beginIndex)参数是起始下标。
1
2
// 截取字符串
System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com
  • (掌握)、String substring(int beginIndex, int endIndex)
1
2
3
// beginIndex起始位置(包括)
// endIndex结束位置(不包括)
System.out.println("http://www.baidu.com".substring(7, 10)); //www
  • (掌握)、char[] toCharArray()
1
2
3
4
5
// 将字符串转换成char数组
char[] chars = "我是中国人".toCharArray();
for(int i = 0; i < chars.length; i++){
System.out.println(chars[i]);
}
  • (掌握)、String toLowerCase()
1
2
// 转换为小写。
System.out.println("ABCDefKXyz".toLowerCase());
  • (掌握)、String toUpperCase();
1
2
//转换为大写。
System.out.println("ABCDefKXyz".toUpperCase());
  • (掌握). String trim();
1
2
// 去除字符串前后空白
System.out.println(" hello world ".trim());
  • (掌握). String中只有一个方法是静态的,不需要new对象:valueOf
1
2
3
4
5
6
7
8
// 作用:将“非字符串”转换成“字符串”
String s1 = String.valueOf(true);
String s1 = String.valueOf(100);
String s1 = String.valueOf(3.14);
String s1 = String.valueOf(new Customer());
//本质上System.out.println()这个方法在输出任何数据的时候都是先调用valueOf转换成字符串,再输出。
System.out.println(s1);// Customer类中没有重写toString()方法之前是对象内存地址

StringBuffer和StringBuilder

因为java中的字符串是不可变的,每一次拼接都会产生新字符串。这样会占用大量的方法区内存。造成内存空间的浪费。

1
2
String s = "abc";
s += "hello";

就以上两行代码,就导致在方法区字符串常量池当中创建了3个对象:

“abc””hello””abchello”

需要进行大量字符串的拼接操作,建议使用JDK中自带的:

1
2
java.lang.StringBuffer
java.lang.StringBuilder

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringBufferTest02 {
public static void main(String[] args) {
// 创建一个初始化容量为16个byte[] 数组。(字符串缓冲区对象)
StringBuffer stringBuffer = new StringBuffer();
// 拼接字符串,以后拼接字符串统一调用 append()方法。
// append是追加的意思。
stringBuffer.append("a");
stringBuffer.append(3.14);
stringBuffer.append(true);
// append方法底层在进行追加的时候,如果byte数组满了,会自动扩容。
// 指定初始化容量的StringBuffer对象(字符串缓冲区对象)
StringBuffer sb = new StringBuffer(100);
sb.append("hello");
System.out.println(sb);
}
}

优化性能:

在创建StringBuffer的时候尽可能给定一个初始化容量,减少底层数组的扩容次数,提高程序的执行效率。

StringBuffer和StringBuilder的区别

​ StringBuffer中的方法都有synchronized关键字修饰。是线程安全的。
​ StringBuilder中的方法都没有synchronized关键字修饰,是非线程安全的。

StringBuilder/StringBuffer可变的原因

StringBuffer/StringBuilder内部实际上是一个byte[]数组,这个byte[]数组没有被final修饰,其初始化容量为16,存满之后会调用了数组拷贝System.arraycopy()…进行扩容。所以StringBuilder/StringBuffer 适合于使用字符串的频繁拼接操作。

而String类中的byte[]数组被final修饰,不可再指向其它对象,arraycopy失效,故String不可变。

基础类型对应的 8 个包装类

包装类的作用:当方法无法接收基本数据类型的数字时,传对应的包装类进去。

8种基本数据类型对应的包装类型名:

1
2
3
4
5
6
7
8
9
10
基本数据类型             包装类型
-------------------------------------
byte java.lang.Byte(父类Number)
short java.lang.Short(父类Number)
int java.lang.Integer(父类Number)
long java.lang.Long(父类Number)
float java.lang.Float(父类Number)
double java.lang.Double(父类Number)
boolean java.lang.Boolean(父类Object)
char java.lang.Character(父类Object)

八种包装类中其中6个都是数字对应的包装类,其父类都是Number,先研究Number中公共的方法:
Number是一个抽象类,无法实例化对象。
Number类中有这样的方法:

1
2
3
4
5
6
byte byteValue()byte 形式返回指定的数值。
abstract double doubleValue()double 形式返回指定的数值。
abstract float floatValue()float 形式返回指定的数值。
abstract int intValue()int 形式返回指定的数值。
abstract long longValue()long 形式返回指定的数值。
short shortValue()short 形式返回指定的数值。

这些方法其实所有的数字包装类的子类都有,负责拆箱。

自动装箱和自动拆箱

在java5之后,引入了一种新特性,自动装箱和自动拆箱

  • 自动装箱:基本数据类型自动转换成包装类。
  • 自动拆箱:包装类自动转换成基本数据类型。

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
这个题目是Integer非常重要的面试题。
*/
public class IntegerTest06 {
public static void main(String[] args) {
Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false
/*
java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,
放到了方法区的“整数型常量池”中,目的是只要用这个区间的数据不需要再new,
直接从整数型常量池当中取出来。
原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存地址是一样的。
*/
Integer x = 127;
Integer y = 127;
// == 永远判断的都是两个对象的内存地址是否相同。
System.out.println(x == y); //true
}
}

Integer的内存结构

Integer类中的常用方法

1
2
3
4
5
6
7
8
9
public class IntegerTest07 {
public static void main(String[] args) {
// 手动装箱
Integer x = new Integer(1000);
// 手动拆箱。
int y = x.intValue();
System.out.println(y);
Integer a = new Integer("123");
// 编译的时候没问题,一切符合java语法,运行时会不会出问题呢?

不是“数字”不能包装成Integer。

1
2
// java.lang.NumberFormatException
Integer a = new Integer("中文");
重点方法:static int parseInt(String s)
1
2
3
4
5
6
7
8
9
10
11
12
// 静态方法,传参String,返回int
//网页上文本框中输入的100实际上是"100"字符串。后台数据库中要求存储100数字,此时java程序需要将"100"转换成100数字。
int retValue = Integer.parseInt("123"); // String -转换-> int
int retValue = Integer.parseInt("中文"); // NumberFormatException
System.out.println(retValue + 100);

// 以下类推
double retValue2 = Double.parseDouble("3.14");
System.out.println(retValue2 + 1); //4.140000000000001(精度问题)

float retValue3 = Float.parseFloat("1.0");
System.out.println(retValue3 + 1); //2.0
了解方法:进制转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//static String toBinaryString(int i)静态的:将十进制转换成二进制字符串。
String binaryString = Integer.toBinaryString(3);
System.out.println(binaryString); //"11" 二进制字符串

//static String toHexString(int i)静态的:将十进制转换成十六进制字符串。
hexString = Integer.toHexString(17);
System.out.println(hexString); // "11"

//static String toOctalString(int i)静态的:将十进制转换成八进制字符串。
String octalString = Integer.toOctalString(8);
System.out.println(octalString); // "10"

System.out.println(new Object()); //java.lang.Object@6e8cf4c6

//valueOf方法
//static Integer valueOf(int i)
// 静态的:int-->Integer
Integer i1 = Integer.valueOf(100);
System.out.println(i1);

// static Integer valueOf(String s)
// 静态的:String-->Integer
Integer i2 = Integer.valueOf("100");
System.out.println(i2);
}
}

使用例:String,int,Integer之间互相转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class IntegerTest08 {
public static void main(String[] args) {

// String --> int
int i1 = Integer.parseInt("100"); // i1是100数字
System.out.println(i1 + 1); // 101

// int --> String
String s2 = i1 + ""; // "100"字符串
System.out.println(s2 + 1); // "1001"

// int --> Integer
// 自动装箱
Integer x = 1000;

// Integer --> int
// 自动拆箱
int y = x;

// String --> Integer
Integer k = Integer.valueOf("123");

// Integer --> String
String e = String.valueOf(k);
}
}

日期相关类

Date类导包

1
2
import java.util.Date;
import java.text.SimpleDateFormat;

获取系统当前时间

1
2
3
4
5
6
7
8
9
10
public class DateTest01 {
public static void main(String[] args) throws Exception {
// 获取系统当前时间(精确到毫秒的系统当前时间)
// 直接调用无参数构造方法就行。
Date nowTime = new Date();

// java.util.Date类的toString()方法已经被重写了。
// 输出的应该不是一个对象的内存地址,应该是一个日期字符串。
//System.out.println(nowTime); //Thu Mar 05 10:51:06 CST 2020

日期格式化且Date —> String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
      // 将日期类型Date,按照指定的格式进行转换:Date --转换成具有一定格式的日期字符串-->String
//SimpleDateFormat是java.text包下的。专门负责日期格式化的。
/*
yyyy 年(年是4位)
MM 月(月是2位)
dd 日
HH 时
mm 分
ss 秒
SSS 毫秒(毫秒3位,最高999。1000毫秒代表1秒)
注意:在日期格式中,除了y M d H m s S这些字符不能随便写之外,剩下的符号格式自己随意组织。
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
//SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
//SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");

String nowTimeStr = sdf.format(nowTime);
System.out.println(nowTimeStr);

日期字符串String转换成Date类型

1
2
3
4
5
6
7
8
9
        // String --> Date
String time = "2008-08-08 08:08:08 888";
//SimpleDateFormat sdf2 = new SimpleDateFormat("格式不能随便写,要和日期字符串格式相同");
// 注意:字符串的日期格式和SimpleDateFormat对象指定的日期格式要一致。不然会出现异常:java.text.ParseException
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date dateTime = sdf2.parse(time);
System.out.println(dateTime); //Fri Aug 08 08:08:08 CST 2008
}
}

简单总结System类的相关属性和方法:

  • System.out 【out是System类的静态变量。】
  • System.out.println() 【println()方法不是System类的,是PrintStream类的方法。】
  • System.gc() 建议启动垃圾回收器
  • System.currentTimeMillis() 获取自1970年1月1日到系统当前时间的总毫秒数。
  • System.exit(0) 退出JVM。

System.currentTimeMillis():获取自1970年1月1日到系统当前时间的总毫秒数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DateTest02 {
public static void main(String[] args) {
// 获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。
long nowTimeMillis = System.currentTimeMillis();
System.out.println(nowTimeMillis); //1583377912981

// 统计一个方法耗时
// 在调用目标方法之前记录一个毫秒数
long begin = System.currentTimeMillis();
print();
// 在执行完目标方法之后记录一个毫秒数
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
// 需求:统计一个方法执行所耗费的时长
public static void print(){
for(int i = 0; i < 1000000000; i++){
System.out.println("i = " + i);
}
}
}

数字相关类

数字类导包

1
import java.text.DecimalFormat;

java.text.DecimalFormat:数字格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DecimalFormatTest01 {
public static void main(String[] args) {
// java.text.DecimalFormat专门负责数字格式化的。
//DecimalFormat df = new DecimalFormat("数字格式");
/*
数字格式有哪些?
# 代表任意数字
, 代表千分位
. 代表小数点
0 代表不够时补0
###,###.##
表示:加入千分位,保留2个小数。
*/
DecimalFormat df = new DecimalFormat("###,###.##");
//String s = df.format(1234.56);
String s = df.format(1234.561232);
System.out.println(s); // "1,234.56"

DecimalFormat df2 = new DecimalFormat("###,###.0000"); //保留4个小数位,不够补上0
String s2 = df2.format(1234.56);
System.out.println(s2); // "1,234.5600"

}
}

财务大数据BigDecimal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BigDecimalTest01 {
public static void main(String[] args) {
// 这个100不是普通的100,是精度极高的100
BigDecimal v1 = new BigDecimal(100);
// 精度极高的200
BigDecimal v2 = new BigDecimal(200);
// 求和
// v1 + v2; // 这样不行,v1和v2都是引用,不能直接使用+求和。
BigDecimal v3 = v1.add(v2); // 调用方法求和。
System.out.println(v3); //300
BigDecimal v4 = v2.divide(v1);
System.out.println(v4); // 2
}
}

Ramdom

导包

1
import java.util.Random;

创建随机数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RandomTest01 {
public static void main(String[] args) {
// 创建随机数对象
Random random = new Random();

// 随机产生一个int类型取值范围内的数字。
int num1 = random.nextInt();

System.out.println(num1);

// 产生[0~100]之间的随机数。不能产生101。
// nextInt翻译为:下一个int类型的数据是101,表示只能取到100.
int num2 = random.nextInt(101); //不包括101
System.out.println(num2);
}
}

Enum枚举类型

枚举是一种引用数据类型,用于结果判断。

结果只有两种情况的,建议使用布尔类型。结果超过两种并且还是可以一枚一枚列举出来的,建议使用枚举类型。例如:颜色、四季、星期等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class EnumTest02 {
public static void main(String[] args) {
Result r = divide(10, 2);
System.out.println(r == Result.SUCCESS ? "计算成功" : "计算失败");
}
/**
* 计算两个int类型数据的商。
* @param a int数据
* @param b int数据
* @return Result.SUCCESS表示成功,Result.FAIL表示失败!
*/
public static Result divide(int a, int b){
try {
int c = a / b;
return Result.SUCCESS;
} catch (Exception e){
return Result.FAIL;
}
}
}
// 枚举:一枚一枚可以列举出来的,才建议使用枚举类型。
// 枚举编译之后也是生成class文件。
// 枚举也是一种引用数据类型。
// 枚举中的每一个值可以看做是常量。
enum Result{
// SUCCESS 是枚举Result类型中的一个值
// FAIL 是枚举Result类型中的一个值
// 枚举中的每一个值,可以看做是“常量”
SUCCESS, FAIL
}

异常类

Exception下有两个分支:
Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错,因此得名编译时异常。)。
RuntimeException:运行时异常。(在编写程序阶段程序员可以预先处理,也可以不管,都行。)

对异常的处理

第一种:如果希望调用者来处理,选择throws上报。在方法声明的位置上,使用throws关键字,抛给上一级。

第二种:使用try..catch语句进行异常的捕捉。

注意

  • 编译时异常和运行时异常,都发生在运行阶段,编译阶段异常是不会发生的。
  • 只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。
  • try语句块中的某一行出现异常,该行后面的代码不会执行。
  • try..catch捕捉异常之后,后续代码可以执行。
  • throws支持多态

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionTest06 {
// 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
// 异常处理机制的作用就是增强程序的健壮性。怎么能做到,异常发生了也不影响程序的执行。所以
// 一般main方法中的异常建议使用try..catch进行捕捉。main就不要继续上抛了。
//public static void main(String[] args) throws FileNotFoundException {
public static void main(String[] args) {
// 100 / 0这是算术异常,这个异常是运行时异常,你在编译阶段,可以处理,也可以不处理。编译器不管。
//System.out.println(100 / 0); // 不处理编译器也不管
System.out.println("main begin");

try..catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
        try {
m1();
// 以上代码出现异常,直接进入catch语句块中执行。
System.out.println("hello world!");
} catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。
// 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。
// catch是捕捉异常之后走的分支。
// 在catch分支中干什么?处理异常。
System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");
System.out.println(e); //java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)
}
//----------------------------------------------------------------------------------------
// try..catch把异常抓住之后,这里的代码会继续执行。
System.out.println("main over");
}

//----------------------------------------------------------------------------------------
private static void m1() throws FileNotFoundException {
System.out.println("m1 begin");
m2();
// 以上代码出异常,这里是无法执行的。
System.out.println("m1 over");
}

throws后面也可以写多个异常,可以使用逗号隔开

1
2
3
4
5
6
7
8
//private static void m2() throws ClassCastException, FileNotFoundException{
private static void m2() throws FileNotFoundException {
System.out.println("m2 begin");
m3();
// 以上如果出现异常,这里是无法执行的!
System.out.println("m2 over");
}

编译报错分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    private static void m3() throws FileNotFoundException {
// 调用SUN jdk中某个类的构造方法。
// 创建一个输入流对象,该流指向一个文件。
/*
编译报错的原因是什么?
第一:这里调用了一个构造方法:FileInputStream(String name)
第二:这个构造方法的声明位置上有:throws FileNotFoundException
第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是 Exception,
最终得知,FileNotFoundException是编译时异常。
错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。
*/
new FileInputStream("D:\\course\\01-课\\学习方法.txt");
// 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。
System.out.println("如果以上代码出异常,这里会执行吗??????????????????不会!!!");
}
}

深入try..catch

  • catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
  • catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
  • catch写多个的时候,从上到下,必须遵守从小到大。

使用例:

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExceptionTest07 {

public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
System.out.println("以上出现异常,这里无法执行!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}
//----------------------------------------------------------------------------------------
try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(Exception e) { // 多态:Exception e = new FileNotFoundException();
System.out.println("文件不存在!");
}

catch可以写多个,从上到下,必须遵守从小到大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
          try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
} catch(IOException e){
System.out.println("读文件报错了!");

//----------------------------------------------------------------------------------------
// 编译报错。
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(IOException e){
System.out.println("读文件报错了!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}

//----------------------------------------------------------------------------------------
// JDK8的新特性
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
// 进行数学运算
System.out.println(100 / 0); // 这个异常是运行时异常,编写程序时可以处理,也可以不处理。
} catch(FileNotFoundException | ArithmeticException | NullPointerException e) {
System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}
}
}

throws和throw的区别

throws:
声明位置

方法名之后

1
2
public void test1() throws NullPointerExeption{
}
作用:

通知开发人员当前方法在运行时,【有可能】抛出的异常。

携带数据

throws后面携带【异常类型】,一个throws后面可以携带多个异常类型。

调用

当一个方法被throws修饰时,调用方法时必须考虑异常捕捉问题。

throw:
声明位置

方法执行体

1
2
3
public void test1 (){
throw new RuntimeException ();
}
作用

throw是一个命令,执行时抛出一个指定异常对象。

携带数据:

throw后面携带【异常对象】,一个throw一次只能携带一个异常对。

调用

当一个方法内部存在throw命令时,在调用时可以不考虑异常捕捉问题。

查看异常的追踪信息

异常对象的两个方法:

1
2
String msg = e.getMessage();
e.printStackTrace(); // 一般都是使用这个。

​ 异常信息追踪信息,从上往下一行一行看。
​ 但是需要注意的是:SUN写的代码就不用看了(看包名)。
​ 主要的问题出现在自己编写的代码上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest09 {
public static void main(String[] args) {
try {
m1();
} catch (FileNotFoundException e) {
// 获取异常的简单描述信息
String msg = e.getMessage();
System.out.println(msg); //C:\jetns-agent.jar (系统找不到指定的文件。)
//打印异常堆栈追踪信息!!!
//在实际的开发中,建议使用这个。
// 这行代码要写上,不然出问题也不知道
e.printStackTrace();
}
// 这里程序不耽误执行,很健壮。
System.out.println("Hello World!");
}
private static void m1() throws FileNotFoundException {
m2();
}
private static void m2() throws FileNotFoundException {
m3();
}
private static void m3() throws FileNotFoundException {
new FileInputStream("C:\\jetns-agent.jar");
}
}

finally

关于try..catch中的finally子句:

  • 在finally子句中的代码是最后执行的,未退出JVM或违反基本规则的前提下,finally子句一定会执行,即使try语句块中的代码出现了异常。
  • finally子句必须和try一起出现,可以没有catch,不能单独编写。

使用例:

例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ExceptionTest11 {
public static void main(String[] args) {

以下代码的执行顺序:
先执行try...
再执行finally...
最后执行 returnreturn语句只要执行方法必然结束。)

try {
System.out.println("try...");
return;
} finally {
// finally中的语句会执行。能执行到。
System.out.println("finally...");
}

这里不能写语句,因为这个代码是无法执行到的。
//System.out.println("Hello World!");
}
}

退出JVM

1
2
3
4
5
6
7
8
9
10
11
public class ExceptionTest12 {
public static void main(String[] args) {
try {
System.out.println("try...");
// 退出JVM
System.exit(0); // 退出JVM之后,finally语句中的代码就不执行了!
} finally {
System.out.println("finally...");
}
}
}

finally面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ExceptionTest13 {
public static void main(String[] args) {
int result = m();
System.out.println(result); //100
}
//----------------------------------------------------------------------------------------
java语法规则(有一些规则不能破坏):
方法体中的代码必须遵循自上而下顺序依次逐行执行。
return语句一旦执行,整个方法必须结束。
//----------------------------------------------------------------------------------------
public static int m(){
int i = 100;
try {
// 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
// return语句还必须保证是最后执行的。一旦执行,整个方法结束。
return i;
} finally {
i++;
}
}
//----------------------------------------------------------------------------------------
反编译之后的效果
public static int m(){
int i = 100;
int j = i;
i++;
return j;
}

}


final finally finalize的区别

  • final:

    final修饰的类无法继承
    final修饰的方法无法覆盖
    final修饰的变量不能重新赋值。
    
  • finally:

    和try一起联合使用,finally语句块中的代码是必须执行的。
    
  • finalize:(过时)

    是一个Object类中的方法名,这个方法是由垃圾回收器GC负责调用的。
    

自定义异常

两步:

  • 第一步:编写一个类继承Exception或者RuntimeException.
  • 第二步:提供两个构造方法,一个无参数的,一个带有String参数的。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void push(Object obj) throws MyStackOperationException {
if(index >= elements.length - 1){
// 改良之前
System.out.println("压栈失败,栈已满!");
return;

// 方案1,创建异常对象,手动将异常抛出去
//MyStackOperationException e = new MyStackOperationException("压栈失败,栈已满!");
//throw e; //这里捕捉没有意义,自己new一个异常,自己捉,没有意义。栈已满这个信息你需要传递 出去。

// 方案2,合并(手动抛出异常!)
throw new MyStackOperationException("压栈失败,栈已满!");
}
}
1
2
3
4
5
6
7
8
public class MyStackOperationException extends Exception{ // 编译时异常!

public MyStackOperationException(){
}
public MyStackOperationException(String s){
super(s);
}
}

集合

  • 集合是一个容器。可以来容纳其它类型的数据。
  • 集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,
  • 集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用。)
  • 所有的集合类和集合接口都在java.util包下。

注意:
集合在java中本身是一个容器,是一个对象。
集合中任何时候存储的都是“引用”。

集合分为两大类:
单个方式存储元素:
单个方式存储元素,这一类集合中超级父接口:java.util.Collection;

Collection

​ 以键值对的方式存储元素
​ 以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map;

Map

Collection接口

1、Collection中能存放什么元素?
没有使用“泛型”之前,Collection中可以存储Object的所有子类型。
使用了“泛型”之后,Collection中只能存储某个具体的类型。
2、Collection中的常用方法

1
2
3
4
5
6
7
boolean add(Object e) 向集合中添加元素
int size() 获取集合中元素的个数
void clear() 清空集合
boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true,不包含返回false
boolean remove(Object o) 删除集合中的某个元素。
boolean isEmpty() 判断该集合中元素的个数是否为0
Object[] toArray() 调用这个方法可以把集合转换成数组。【作为了解,使用不多。】

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.util.ArrayList;
import java.util.Collection;
public class CollectionTest01 {
public static void main(String[] args) {
// 创建一个集合对象
//Collection c = new Collection(); // 接口是抽象的,无法实例化。
// 多态
Collection c = new ArrayList();
// 测试Collection接口中的常用方法
c.add(1200); // 自动装箱(java5的新特性。),实际上是放进去了一个对象的内存地址。Integer x = new Integer(1200);
c.add(3.14); // 自动装箱
c.add(new Object());
c.add(new Student());
c.add(true); // 自动装箱
//----------------------------------------------------------------------------------------
// 获取集合中元素的个数
System.out.println("集合中元素个数是:" + c.size()); // 5
//----------------------------------------------------------------------------------------
// 清空集合
c.clear();
System.out.println("集合中元素个数是:" + c.size()); // 0
//----------------------------------------------------------------------------------------
// 再向集合中添加元素
c.add("hello"); // "hello"对象的内存地址放到了集合当中。
c.add("world");
c.add("浩克");
c.add("绿巨人");
c.add(1);
//----------------------------------------------------------------------------------------
// 判断集合中是否包含"绿巨人"
boolean flag = c.contains("绿巨人");
System.out.println(flag); // true
boolean flag2 = c.contains("绿巨人2");
System.out.println(flag2); // false
System.out.println(c.contains(1)); // true
//----------------------------------------------------------------------------------------
System.out.println("集合中元素个数是:" + c.size()); // 5

// 删除集合中某个元素
c.remove(1);
System.out.println("集合中元素个数是:" + c.size()); // 4
//----------------------------------------------------------------------------------------
// 判断集合是否为空(集合中是否存在元素)
System.out.println(c.isEmpty()); // false
// 清空
c.clear();
System.out.println(c.isEmpty()); // true(true表示集合中没有元素了!)

c.add("abc");
c.add("def");
c.add(100);
c.add("helloworld!");
c.add(new Student());
//----------------------------------------------------------------------------------------
// 转换成数组(了解,使用不多。)
Object[] objs = c.toArray();
for(int i = 0; i < objs.length; i++){
// 遍历数组
Object o = objs[i];
System.out.println(o);
}
}
}
class Student{

}

集合遍历/迭代

注意:以下讲解的遍历方式/迭代方式,在所有的Collection以及子类中使用,不能用在Map集合中。

以下两个方法是迭代器对象Iterator中的方法:

1
2
boolean hasNext()如果仍有元素可以迭代,则返回 true
Object next() 返回迭代的下一个元素。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class CollectionTest02 {
public static void main(String[] args) {
// 创建集合对象
Collection c1 = new ArrayList(); // ArrayList集合:有序可重复
// 添加元素
c1.add(1);
c1.add(2);
c1.add(3);
c1.add(4);
c1.add(1);
// 迭代集合
Iterator it = c1.iterator();
while(it.hasNext()){
// 存进去是什么类型,取出来还是什么类型。
Object obj = it.next();
if(obj instanceof Integer){
System.out.println("Integer类型");
}
// 只不过在输出的时候会转换成字符串。因为这里println会调用toString()方法。
System.out.println(obj);
}

Collection c2 = new HashSet();// HashSet集合:无序不可重复
// 无序:存进去和取出的顺序不一定相同。
// 不可重复:相同值覆盖。
c2.add(100);
c2.add(200);
c2.add(300);
c2.add(90);
c2.add(400);
c2.add(50);
c2.add(60);
c2.add(100);
Iterator it2 = c2.iterator();
while(it2.hasNext()){
System.out.println(it2.next());
}
}
}

注意:

Collection集合的contains方法和remove方法都调用了equals方法进行比对。equals方法返回true,就表示包含这个元素。因此存放在一个集合中的类型,一定要重写equals方法。

关于集合元素的remove

重点:

  • 当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器,会出现
    异常:java.util.ConcurrentModificationException

  • 在迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:

    c.remove(o); 迭代过程中禁止这样写。
    会出现:java.util.ConcurrentModificationException
    
  • 在迭代元素的过程当中,一定要使用迭代器Iterator的remove方法,删除元素,
    不要使用集合自带的remove方法删除元素。

1
2
3
4
5
6
7
8
9
while(it2.hasNext()){
Object o = it2.next();
Collection c2 = new ArrayList();
// 出异常根本原因是:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了)
c2.remove(o); // 直接通过集合去删除元素,没有通知迭代器。(导致迭代器的快照和原集合状态不 同。)
Iterator it2 = c2.iterator();
// 迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。
it2.remove(); // 删除的一定是迭代器指向的当前元素。
}

List接口

  • List集合存储元素特点:有序可重复
        有序:List集合中的元素有下标,子类也继承该特性。 从0开始,以1递增。
        可重复:存储一个1,还可以再存储1.
    
List接口特色常用方法
1
2
3
4
5
6
void add(int index, Object element)
Object set(int index, Object element)
Object get(int index)
int indexOf(Object o)
int lastIndexOf(Object o)
Object remove(int index)

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class ListTest01 {
public static void main(String[] args) {
// 创建List类型的集合。
//List myList = new LinkedList();
//List myList = new Vector();
List myList = new ArrayList();
//----------------------------------------------------------------------------------------
// 添加元素
myList.add("A"); // 默认都是向集合末尾添加元素。
myList.add("B");
myList.add("C");
myList.add("C");
myList.add("D");
//----------------------------------------------------------------------------------------
//在列表的指定位置插入指定元素(第一个参数是下标)
// 这个方法使用不多,因为对于ArrayList集合来说效率比较低。
myList.add(1, "KING");
//----------------------------------------------------------------------------------------
// 迭代
Iterator it = myList.iterator();
while(it.hasNext()){
Object elt = it.next();
System.out.println(elt);
}
//----------------------------------------------------------------------------------------
// 根据下标获取元素
Object firstObj = myList.get(0);
System.out.println(firstObj);
// 因为有下标,所以List集合有自己比较特殊的遍历方式
// 通过下标遍历。【List集合特有的方式,Set没有。】
for(int i = 0; i < myList.size(); i++){
Object obj = myList.get(i);
System.out.println(obj);
}
//----------------------------------------------------------------------------------------
// 获取指定对象第一次出现处的索引。
System.out.println(myList.indexOf("C")); // 3
//----------------------------------------------------------------------------------------
// 获取指定对象最后一次出现处的索引。
System.out.println(myList.lastIndexOf("C")); // 4
//----------------------------------------------------------------------------------------
// 删除指定下标位置的元素
// 删除下标为0的元素
myList.remove(0);
System.out.println(myList.size()); // 5
System.out.println("====================================");
//----------------------------------------------------------------------------------------
// 修改指定位置的元素
myList.set(2, "Soft");
}
}
ArrayList集合
构造方法:
1
2
new ArrayList();
new ArrayList(20);

使用例:

1
2
3
4
5
6
// 创建一个HashSet集合
Collection c = new HashSet();
// 通过这个构造方法就可以将HashSet集合转换成List集合。
List myList3 = new ArrayList(c);
for(int i = 0; i < myList3.size(); i++){
System.out.println(myList3.get(i));
特点:
  • 默认初始化容量10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。)
  • 集合底层是一个Object[]数组。
  • ArrayList集合的扩容:增长到原容量的1.5倍。
  • ArrayList集合是非线程安全的。
  • ArrayList集合底层是数组,所以应尽可能少的扩容。

面试官经常问的一个问题?
这么多的集合中,你用哪个集合最多?
答:ArrayList集合。
因为往数组末尾添加元素,效率不受影响。
另外,我们检索/查找某个元素的操作比较多。

LinkedList集合

LinkedList底层使用双向链表,随机增删效率较高,检索/查找的效率较低。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// LinkedList集合有初始化容量吗?没有。
// 最初这个链表中没有任何元素。first和last引用都是null。
// 不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合。
// 因为面向接口编程,调用的方法都是接口中的方法。
List list2 = new ArrayList(); // 这样写表示底层用数组。
List list2 = new LinkedList(); // 这样写表示底层用双向链表。
// 以下方法面向接口编程。
list2.add("123");
list2.add("456");
list2.add("789");
for(int i = 0; i < list2.size(); i++){
System.out.println(list2.get(i));
}
Vector
特点:
  • 底层也是一个数组。
  • 初始化容量:10
  • 扩容之后是原容量的2倍。
  • Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的。效率较低,使用较少。

扩展:将一个线程不安全的ArrayList集合转换成线程安全的

使用集合工具类:
java.util.Collections;

        java.util.Collection 是集合接口。
        java.util.Collections 是集合工具类。
1
2
List myList = new ArrayList(); // 非线程安全的。
Collections.synchronizedList(myList);// 变成线程安全的

foreach

JDK5.0之后推出的新特性:叫做增强for循环,或者叫做foreach

缺点:没有下标。在需要使用下标的循环中,不建议使用增强for循环。

语法:

1
2
3
for(元素类型 变量名 : 数组或集合){
System.out.println(变量名);
}

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ForEachTest02 {
public static void main(String[] args) {
// 创建List集合
List<String> strList = new ArrayList<>();

// 添加元素
strList.add("hello");
strList.add("world!");
strList.add("kitty!");

// 遍历,使用迭代器方式
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String s = it.next();
System.out.println(s);
}

// 使用下标方式(只针对于有下标的集合)
for(int i = 0; i < strList.size(); i++){
System.out.println(strList.get(i));
}

// 使用foreach
for(String s : strList){ // 因为泛型使用的是String类型,所以是:String s
System.out.println(s);
}

List<Integer> list = new ArrayList<>();
list.add(100);
list.add(200);
list.add(300);
for(Integer i : list){ // i代表集合中的元素
System.out.println(i);
}
}
}

泛型

  • JDK5.0之后推出的新特性。
  • 泛型这种语法机制,只在程序编译阶段起作用,给编译器作参考。

优点

  • 集合中存储的元素类型统一了。
  • 从集合中取出的元素类型是泛型指定的类型,不需要进行向下转型。

缺点
导致集合中存储的元素缺乏多样性。

自动类型推断机制

JDK8之后引入了自动类型推断机制(又称钻石表达式)。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class GenericTest02 {
public static void main(String[] args) {
// ArrayList<这里的类型会自动推断>(),前提是JDK8之后才允许。
// 自动类型推断,原本为List<String> strList = new ArrayList<String>();
List<String> strList = new ArrayList<>();
// 类型不匹配。
//strList.add(new Cat());
strList.add("http://www.126.com");
strList.add("http://www.baidu.com");
strList.add("http://www.bjpowernode.com");
// 类型不匹配。
//strList.add(123);
//System.out.println(strList.size());

// 遍历
Iterator<String> it2 = strList.iterator();
while(it2.hasNext()){
// 如果没有使用泛型
/*
Object obj = it2.next();
if(obj instanceof String){
String ss = (String)obj;
ss.substring(7);
}
*/
// 直接通过迭代器获取了String类型的数据
String s = it2.next();
// 直接调用String类的substring方法截取字符串。
String newString = s.substring(7);
System.out.println(newString);
}
}
}

自定义泛型

自定义泛型的时候,<> 尖括号中的标识符可自定义。在java源代码中经常使用 ,E是Element单词首字母,T是Type单词首字母。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class GenericTest03<标识符随便写> {

public void doSome(标识符随便写 o){
System.out.println(o);
}

public static void main(String[] args) {

// new对象的时候指定了泛型是:String类型
GenericTest03<String> gt = new GenericTest03<>();
// 类型不匹配
//gt.doSome(100);
gt.doSome("abc");

MyIterator<String> mi = new MyIterator<>();
String s1 = mi.get();

// 不用泛型就是Object类型。
/*GenericTest03 gt3 = new GenericTest03();
gt3.doSome(new Object());*/
}
}
class MyIterator<T> {
public T get(){
return null;
}
}

Map接口

Map和Collection没有继承关系。
Map集合以key和value的方式存储数据:键值对
key和value都是引用数据类型。
key和value都是存储对象的内存地址。
key起主导作用,value附属。

常用方法:

1
2
3
4
5
6
7
8
9
10
11
   V put(K key, V value) 					向Map集合中添加键值对
V get(Object key) 通过key获取value
void clear() 清空Map集合
boolean containsKey(Object key) 判断Map中是否包含某个key
boolean containsValue(Object value) 判断Map中是否包含某个value
boolean isEmpty() 判断Map集合中元素个数是否为0
V remove(Object key) 通过key删除键值对
int size() 获取Map集合中键值对的个数。
Collection<V> values() 获取Map集合中所有的value,返回一个Collection
Set<K> keySet() 获取Map集合所有的key(所有的键是一个set集合)
Set<Map.Entry<K,V>> entrySet() 将Map集合转换成Set集合

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MapTest01 {
public static void main(String[] args) {
// 创建Map集合对象
Map<Integer, String> map = new HashMap<>();
// 向Map集合中添加键值对
map.put(1, "zhangsan"); // 1在这里进行了自动装箱。
map.put(2, "lisi");
map.put(3, "wangwu");
map.put(4, "zhaoliu");
// 通过key获取value
String value = map.get(2);
System.out.println(value);
// 获取键值对的数量
System.out.println("键值对的数量:" + map.size());
// 通过key删除key-value
map.remove(2);
System.out.println("键值对的数量:" + map.size());
// 判断是否包含某个key
// contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。
System.out.println(map.containsKey(new Integer(4))); // true
// 判断是否包含某个value
System.out.println(map.containsValue(new String("wangwu"))); // true

// 获取所有的value
Collection<String> values = map.values();
// foreach
for(String s : values){
System.out.println(s);
}

// 清空map集合
map.clear();
System.out.println("键值对的数量:" + map.size());
// 判断是否为空
System.out.println(map.isEmpty()); // true
}
}

Map集合的遍历

第一种方式:获取所有的key,通过遍历key,来遍历value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Map<Integer, String> map = new HashMap<>();
map.put(1, "zhangsan");
map.put(2, "lisi");
map.put(3, "wangwu");
map.put(4, "zhaoliu");
// 遍历Map集合
// 获取所有的key,所有的key是一个Set集合
Set<Integer> keys = map.keySet();
// 遍历key,通过key获取value
/*迭代器
Iterator<Integer> it = keys.iterator();
while(it.hasNext()){
// 取出其中一个key
Integer key = it.next();
// 通过key获取value
String value = map.get(key);
System.out.println(key + "=" + value);
}
*/
// foreach
for(Integer key : keys){
System.out.println(key + "=" + map.get(key));
}

第二种方式:Set<Map.Entry<K,V>> entrySet()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 把Map集合直接全部转换成Set集合。
// Set集合中元素的类型是:Map.Entry,是Map中的静态内部类
Set<Map.Entry<Integer,String>> set = map.entrySet();
// 遍历Set集合,每一次取出一个Node
/*迭代器
Iterator<Map.Entry<Integer,String>> it2 = set.iterator();
while(it2.hasNext()){
Map.Entry<Integer,String> node = it2.next();
Integer key = node.getKey();
String value = node.getValue();
System.out.println(key + "=" + value);
}
*/
// foreach
// 这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。
// 这种方式比较适合于大数据量。
for(Map.Entry<Integer,String> node : set){
System.out.println(node.getKey() + "--->" + node.getValue());
}

HashMap集合

哈希表或者散列表数据结构

HashSet集合

无序不可重复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class HashSetTest01 {
public static void main(String[] args) {
// 演示一下HashSet集合特点
Set<String> strs = new HashSet<>();
// 添加元素
strs.add("hello3");
strs.add("hello4");
strs.add("hello1");
strs.add("hello2");
strs.add("hello3");
strs.add("hello3");
strs.add("hello3");
strs.add("hello3");
/*遍历
hello1
hello4
hello2
hello3
1、存储时顺序和取出的顺序不同。
2、不可重复。
3、放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。
*/
for(String s : strs){
System.out.println(s);
}
}
}
HashMap性质:
  • HashMap集合底层是哈希表/散列表的数据结构。

​ 哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。

  • HashMap集合的默认初始化容量是16,默认加载因子是0.75:当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。

  • 向Map集合中存或取,都先调用key的hashCode方法,然后再调用equals方法。当数组下标位置上是null时,equals方法不调用。

  • HashMap集合key部分允许null,但只能有一个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class HashMapTest03 {
    public static void main(String[] args) {

    Map map = new HashMap();

    // HashMap集合允许key为null
    map.put(null, null);
    System.out.println(map.size()); // 1

    // key重复的话value是覆盖!
    map.put(null, 100);
    System.out.println(map.size()); //1

    // 通过key获取value
    System.out.println(map.get(null)); // 100
    }
    }

重点:
HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是为了达到散列均匀,提高HashMap集合的存取效率。

HashMap集合底层源代码:
1
2
3
4
5
6
7
8
9
10
11
public class HashMap{
// HashMap底层实际上就是一个数组。(一维数组)
Node<K,V>[] table;
// 静态的内部类HashMap.Node
static class Node<K,V> {
final int hash; // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
final K key; // 存储到Map集合中的那个key
V value; // 存储到Map集合中的那个value
Node<K,V> next; // 下一个节点的内存地址。
}
}

掌握这两个方法的实现原理。

1
2
map.put(k,v)
v = map.get(k)
HashMap集合的key部分特点:
  • 无序:不一定挂到哪个单向链表上。
  • 不可重复: equals方法来保证HashMap集合的key不可重复,如果key重复了,value会覆盖。

注意:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

哈希表HashMap使用不当时无法发挥性能:

​ hashCode()方法返回值固定为某个值:底层哈希表变成了纯单向链表。这种情况为散列分布不均匀。
hashCode()方法返回值都设定为不一样的值:底层哈希表成为一维数组,也是散列分布不均匀。
散列分布均匀需要重写hashCode()方法时有一定的技巧。

Hashtable

  • Hashtable和HashMap一样,底层都是哈希表数据结构。
  • 初始化容量是11,默认加载因子是:0.75f。
  • 扩容:原容量 * 2 + 1。
  • Hashtable的key和value都不能为null。
  • Hashtable方法都带有synchronized:线程安全的。线程安全有其它的方案,Hashtable对线程的处理效率较低,使用较少。
Properties
  • Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
  • Properties被称为属性类对象。是线程安全的。

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PropertiesTest01 {
public static void main(String[] args) {
// 创建一个Properties对象
Properties pro = new Properties();
// 需要掌握Properties的两个方法,一个存,一个取。
pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode");
pro.setProperty("driver","com.mysql.jdbc.Driver");
pro.setProperty("username", "root");
pro.setProperty("password", "123");

// 通过key获取value
String url = pro.getProperty("url");
String driver = pro.getProperty("driver");
String username = pro.getProperty("username");
String password = pro.getProperty("password");

System.out.println(url);
System.out.println(driver);
System.out.println(username);
System.out.println(password);

}
}

比较接口

TreeSet

  • TreeSet集合底层是TreeMap,TreeMap集合底层是一个二叉树。

  • 放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。

  • TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。称为:可排序集合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class TreeSetTest02 {
    public static void main(String[] args) {
    // 创建一个TreeSet集合
    TreeSet<String> ts = new TreeSet<>();
    // 添加String
    ts.add("zhangsan");
    ts.add("lisi");
    ts.add("wangwu");
    ts.add("zhangsi");
    ts.add("wangliu");
    // 遍历
    for(String s : ts){
    // 按照字典顺序,升序!
    System.out.println(s);
    }

    TreeSet<Integer> ts2 = new TreeSet<>();
    ts2.add(100);
    ts2.add(200);
    ts2.add(900);
    ts2.add(800);
    ts2.add(600);
    ts2.add(10);
    for(Integer elt : ts2){
    // 升序!
    System.out.println(elt);
    }
    }
    }

实现自定义类型比较

放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:

方式一:实现java.lang.Comparable接口,并且实现compareTo方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.TreeSet;
/*
先按照年龄升序,如果年龄一样的再按照姓名升序。
*/
public class TreeSetTest05 {
public static void main(String[] args) {
TreeSet<Vip> vips = new TreeSet<>();
vips.add(new Vip("zhangsi", 20));
vips.add(new Vip("zhangsan", 20));
vips.add(new Vip("king", 18));
vips.add(new Vip("soft", 17));
for(Vip vip : vips){
System.out.println(vip);
}
}
}

class Vip implements Comparable<Vip>{
String name;
int age;

public Vip(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Vip{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
/*
compareTo方法的返回值很重要:
返回0表示相同,value会覆盖。
返回>0,会继续在右子树上找。【10 - 9 = 1 ,1 > 0的说明左边这个数字比较大。所以在右子树上找。】
返回<0,会继续在左子树上找。
*/
@Override
public int compareTo(Vip v) {
// 写排序规则,按照什么进行比较。
if(this.age == v.age){
// 年龄相同时按照名字排序。
// 姓名是String类型,可以直接比。调用compareTo来完成比较。
return this.name.compareTo(v.name);
} else {
// 年龄不一样
return this.age - v.age;
}
}
}

方式二:使用比较器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetTest06 {
public static void main(String[] args) {
// 创建TreeSet集合的时候,需要使用这个比较器。
// TreeSet<WuGui> wuGuis = new TreeSet<>();//这样不行,没有通过构造方法传递一个比较器进去。
// 给构造方法传递一个比较器。
TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator());
/*可以使用匿名内部类的方式(这个类没有名字。直接new接口。)
TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() {
@Override
public int compare(WuGui o1, WuGui o2) {
return o1.age - o2.age;
}
});
*/
wuGuis.add(new WuGui(1000));
wuGuis.add(new WuGui(800));
wuGuis.add(new WuGui(810));
for(WuGui wuGui : wuGuis){
System.out.println(wuGui);
}
}
}

class WuGui{
int age;
public WuGui(int age){
this.age = age;
}
@Override
public String toString() {
return "小乌龟[" +
"age=" + age +
']';
}
}

// 单独在这里编写一个比较器
// 比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。)
class WuGuiComparator implements Comparator<WuGui> {
@Override
public int compare(WuGui o1, WuGui o2) {
// 指定比较规则
// 按照年龄排序
return o1.age - o2.age;
}
}

区分:
  • 当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。

  • 如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。因为可以写多个Comparator,分别对应不同规则,按需调用。

集合工具类

使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.*;
public class CollectionsTest {
public static void main(String[] args) {
// ArrayList集合不是线程安全的。
List<String> list = new ArrayList<>();
// 变成线程安全的
Collections.synchronizedList(list);
// 排序
list.add("abf");
list.add("abx");
list.add("abc");
list.add("abe");
Collections.sort(list);
for(String s : list){
System.out.println(s);
}

List<WuGui2> wuGuis = new ArrayList<>();
wuGuis.add(new WuGui2(1000));
wuGuis.add(new WuGui2(8000));
wuGuis.add(new WuGui2(500));
// 注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。
Collections.sort(wuGuis);
for(WuGui2 wg : wuGuis){
System.out.println(wg);
}

// 对Set集合怎么排序呢?
Set<String> set = new HashSet<>();
set.add("king");
set.add("kingsoft");
set.add("king2");
set.add("king1");
// 将Set集合转换成List集合
List<String> myList = new ArrayList<>(set);
Collections.sort(myList);
for(String s : myList) {
System.out.println(s);
}
// 这种方式也可以排序。
//Collections.sort(list集合, 比较器对象);
}
}

class WuGui2 implements Comparable<WuGui2>{
int age;
public WuGui2(int age){
this.age = age;
}
@Override
public int compareTo(WuGui2 o) {
return this.age - o.age;
}
@Override
public String toString() {
return "WuGui2{" +
"age=" + age +
'}';
}
}

IO流

IO流按照读取数据方式不同进行分类:

  • 有的流是按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。可以读取所有类型文件。

  • 有的流是按照字符的方式读取数据,一次读取一个字符,仅读取普通文本文件。

  • java中所有的流都是在:java.io.*;下。
  • 在java中“类名”以Stream结尾的都是字节流。以“Reader/Writer”结尾的都是字符流。

四大类:

1
2
3
4
java.io.InputStream  字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流

特点:

  • 都是抽象类。(abstract class)

  • 所有的流都实现了:java.io.Closeable接口,都有close()方法。

​ 流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(占用)很多资源。

  • 所有的输出流都实现了:java.io.Flushable接口,都有flush()方法。

输出流在最终输出之后,一定要记得flush()刷新。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完。刷新的作用就是清空管道。
注意:如果没有flush()可能会导致丢失数据。

java.io包下需要掌握的16个流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
文件专属:
java.io.FileInputStream(掌握)
java.io.FileOutputStream(掌握)
java.io.FileReader
java.io.FileWriter

转换流:(将字节流转换成字符流)
java.io.InputStreamReader
java.io.OutputStreamWriter

缓冲流专属:
java.io.BufferedReader
java.io.BufferedWriter
java.io.BufferedInputStream
java.io.BufferedOutputStream

数据流专属:
java.io.DataInputStream
java.io.DataOutputStream

标准输出流:
java.io.PrintWriter
java.io.PrintStream(掌握)

对象专属流:
java.io.ObjectInputStream(掌握)
java.io.ObjectOutputStream(掌握)

FileInputStream

  • 文件字节输入流,任何类型的文件都可以采用这个流来读。
  • 字节的方式,完成输入的操作,完成读的操作(硬盘—> 内存)
常用方法:

返回值是读取到的“字节”本身:

  • int read();
    
    1
    2
    3
    4
    5

    返回值是读取到的字节数量。一次最多读取 b.length 个字节。

    - ```java
    int read(byte[] b);
    ​ 减少硬盘和内存的交互,提高程序的执行效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStreamTest04 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("chapter23/src/tempfile3");
// 准备一个byte数组
byte[] bytes = new byte[4];
int readCount = 0;
while((readCount = fis.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, readCount));
}

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

返回流当中剩余的没有读到的字节数量:

  • int available();
    
    1
    2
    3
    4
    5

    跳过几个字节不读:

    - ```JAVA
    long skip(long n);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    public class FileInputStreamTest05 {
    public static void main(String[] args) {
    FileInputStream fis = null;
    try {
    fis = new FileInputStream("tempfile");
    System.out.println("总字节数量:" + fis.available());
    // 读1个字节
    //int readByte = fis.read();
    // 还剩下可以读的字节数量是:5
    //System.out.println("剩下多少个字节没有读:" + fis.available());
    // 这个方法有什么用?
    //byte[] bytes = new byte[fis.available()]; // 这种方式不太适合太大的文件,因为byte[]数组不能太大。
    // 不需要循环了。
    // 直接读一次就行了。
    //int readCount = fis.read(bytes); // 6
    //System.out.println(new String(bytes)); // abcdef
    //----------------------------------------------------------------------------------------
    // skip跳过几个字节不读取,这个方法也可能以后会用!
    fis.skip(3);
    System.out.println(fis.read()); //100

    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    if (fis != null) {
    try {
    fis.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }
    }
    #### FileOutputStream;
  • 文件字节输出流,负责写。
  • 从内存到硬盘。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamTest01 {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
// myfile文件不存在的时候会自动新建!
// 这种方式谨慎使用,这种方式会先将原文件清空,然后重新写入。
//fos = new FileOutputStream("myfile");
//fos = new FileOutputStream("chapter23/src/tempfile3");

// 以追加的方式在文件末尾写入。不会清空原文件内容。
fos = new FileOutputStream("chapter23/src/tempfile3", true);
// 开始写。
byte[] bytes = {97, 98, 99, 100};
// 将byte数组全部写出!
fos.write(bytes); // abcd
// 将byte数组的一部分写出!
fos.write(bytes, 0, 2); // 再写出ab

// 字符串
String s = "我是一个中国人,我骄傲!!!";
// 将字符串转换成byte数组。
byte[] bs = s.getBytes();
// 写
fos.write(bs);

// 写完之后,最后一定要刷新
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

FileReader和FileWriter

类似,只能拷贝“普通文本”文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class Copy02 {
public static void main(String[] args) {
FileReader in = null;
FileWriter out = null;
try {
// 读
in = new FileReader("java/io/Copy02.java");
// 写
out = new FileWriter("Copy02.java");

// 一边读一边写:
char[] chars = new char[1024 * 512]; // 1MB
int readCount = 0;
while((readCount = in.read(chars)) != -1){
out.write(chars, 0, readCount);
}

// 刷新
out.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

}
}

BufferedReader

带有缓冲区的字符输入流。使用这个流的时候不需要自定义char数组,byte数组。自带缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
public class BufferedReaderTest02 {
public static void main(String[] args) throws Exception{
// 当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流。
// 外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流。
// 像当前这个程序来说:FileReader就是一个节点流。BufferedReader就是包装流/处理流。
/*// 字节流
FileInputStream in = new FileInputStream("Copy02.java");

// 通过转换流转换(InputStreamReader将字节流转换成字符流。)
// in是节点流。reader是包装流。
InputStreamReader reader = new InputStreamReader(in);
// 这个构造方法只能传一个字符流。不能传字节流。
// reader是节点流。br是包装流。
BufferedReader br = new BufferedReader(reader);*/
// 合并
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("Copy02.java")));

String line = null;
while((line = br.readLine()) != null){
System.out.println(line);
}
// 关闭最外层
br.close();
}
}

BufferedWriter:

带有缓冲的字符输出流。

OutputStreamWriter:转换流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;
public class BufferedWriterTest {
public static void main(String[] args) throws Exception{
// 带有缓冲区的字符输出流
//BufferedWriter out = new BufferedWriter(new FileWriter("copy"));

BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy", true)));
// 开始写。
out.write("hello world!");
out.write("\n");
out.write("hello kitty!");
// 刷新
out.flush();
// 关闭最外层
out.close();
}
}

DataInputStream和DataOutputStream

  • DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候需要提前知道写入的顺序。读的顺序需要和写的顺序一致。才可以正常取出数据。

  • DataOutputStream:数据专属的流。这个流可以将数据连同数据的类型一并写入文件。这个文件不是普通文本文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;

public class DataInputStreamTest01 {
public static void main(String[] args) throws Exception{

// 创建数据专属的字节输出流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
// 写数据
byte b = 100;
short s = 200;
int i = 300;
long l = 400L;
// 写
dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);

// 刷新
dos.flush();
// 关闭最外层
dos.close();

//----------------------------------------------------------------------------------------
DataInputStream dis = new DataInputStream(new FileInputStream("data"));
// 开始读
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
System.out.println(b);
System.out.println(s);
System.out.println(i);
System.out.println(l);
dis.close();
}
}

PrintStream

标准的字节输出流。默认输出到控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PrintStreamTest {
public static void main(String[] args) throws Exception{
// 联合起来写
System.out.println("hello world!");
// 分开写
PrintStream ps = System.out;
ps.println("hello world!");
// 标准输出流不需要手动close()关闭。
// 可以改变标准输出流的输出方向吗? 可以

// 标准输出流不再指向控制台,指向“log”文件。
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
// 修改输出方向,将输出方向修改到"log"文件。
System.setOut(printStream);
// 再输出
System.out.println("hello world");
System.out.println("hello kitty");
System.out.println("hello zhangsan");
}
}

应用:日志文件生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class LogTest {
public static void main(String[] args) {
//测试工具类是否好用
Logger.log("调用了System类的gc()方法,建议启动垃圾回收");
Logger.log("调用了UserService的doSome()方法");
Logger.log("用户尝试进行登录,验证失败");
}
}
public class Logger {
/*
记录日志的方法。
*/
public static void log(String msg) {
try {
// 指向一个日志文件
PrintStream out = new PrintStream(new FileOutputStream("log.txt", true));
// 改变输出方向
System.setOut(out);
// 日期当前时间
Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowTime);

System.out.println(strTime + ": " + msg);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}

序列化

java语言中是采用什么机制来区分类的?

​ 第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类。
​ 第二:如果类名一样,再怎么进行类的区别?靠序列化版本号进行区分。

结论
  • 参与序列化和反序列化的对象,必须实现Serializable接口。

  • 实现Serializable接口起到标识的作用,会为该类自动生成一个序列化版本号。

  • 凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号。这样,即使以后这个类代码修改了,但版本号不变,java虚拟机会认为是同一个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
/*
一次序列化多个对象
提示:
参与序列化的ArrayList集合以及集合中的元素User都需要实现 java.io.Serializable接口。
*/
public class ObjectOutputStreamTest02 {
public static void main(String[] args) throws Exception{
List<User> userList = new ArrayList<>();
userList.add(new User(1,"zhangsan"));
userList.add(new User(2, "lisi"));
userList.add(new User(3, "wangwu"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users"));

// 序列化一个集合,这个集合对象中放了很多其他对象。
oos.writeObject(userList);

oos.flush();
oos.close();
}
}
/*
反序列化集合
*/
public class ObjectInputStreamTest02 {
public static void main(String[] args) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users"));
//Object obj = ois.readObject();
//System.out.println(obj instanceof List);
List<User> userList = (List<User>)ois.readObject();
for(User user : userList){
System.out.println(user);
}
ois.close();
}
}
transient

关键字,表示不参与序列化。

1
private transient String name; // name不参与序列化操作!

IO+Properties的联合应用

经常改变的数据,可以单独写到一个文件中,使用程序动态读取。
类似于以上机制的这种文件被称为配置文件。并且当配置文件中的内容格式是:

1
2
key1=value
key2=value

我们把这种配置文件叫做属性配置文件。属性配置文件建议以.properties结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class IoPropertiesTest01 {
public static void main(String[] args) throws Exception{
/*
Properties是一个Map集合,key和value都是String类型。
想将userinfo文件中的数据加载到Properties对象当中。
*/
// 新建一个输入流对象
FileReader reader = new FileReader("chapter23/userinfo.properties");

// 新建一个Map集合
Properties pro = new Properties();

// 调用Properties对象的load方法将文件中的数据加载到Map集合中。
pro.load(reader); // 文件中的数据顺着管道加载到Map集合中,其中等号=左边做key,右边做value

// 通过key来获取value
String username = pro.getProperty("username");
System.out.println(username);
String password = pro.getProperty("password");
System.out.println(password);
String data = pro.getProperty("data");
System.out.println(data);
}
}

File类

File是一个路径名的抽象表示形式。一个File对象有可能对应的是目录,也可能是文件。

File类的常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.File;
public class FileTest01 {
public static void main(String[] args) throws Exception {
// 创建一个File对象
File f1 = new File("D:\\file");
// 判断是否存在!
System.out.println(f1.exists());
// 如果D:\file不存在,则以文件的形式创建出来
/*if(!f1.exists()) {
// 以文件形式新建
f1.createNewFile();
}*/

// 如果D:\file不存在,则以目录的形式创建出来
/*if(!f1.exists()) {
// 以目录的形式新建。
f1.mkdir();
}*/

// 可以创建多重目录吗?
File f2 = new File("D:/a/b/c/d/e/f");
/*if(!f2.exists()) {
// 多重目录的形式新建。
f2.mkdirs();
}*/

File f3 = new File("D:\\course\\01-开课\\学习方法.txt");
// 获取文件的父路径
String parentPath = f3.getParent();
System.out.println(parentPath); //D:\course\01-开课
File parentFile = f3.getParentFile();
System.out.println("获取绝对路径:" + parentFile.getAbsolutePath());

File f4 = new File("copy");
System.out.println("绝对路径:" + f4.getAbsolutePath()); // C:\Users\Administrator\IdeaProjects\javase\copy

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FileTest02 {
public static void main(String[] args) {

File f1 = new File("D:\\course\\01-开课\\开学典礼.ppt");
// 获取文件名
System.out.println("文件名:" + f1.getName());

// 判断是否是一个目录
System.out.println(f1.isDirectory()); // false

// 判断是否是一个文件
System.out.println(f1.isFile()); // true

// 获取文件最后一次修改时间
long haoMiao = f1.lastModified(); // 这个毫秒是从1970年到现在的总毫秒数。
// 将总毫秒数转换成日期?????
Date time = new Date(haoMiao);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
System.out.println(strTime);

// 获取文件大小
System.out.println(f1.length()); //216064字节。
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class FileTest03 {
public static void main(String[] args) {
// File[] listFiles()
// 获取当前目录下所有的子文件。
File f = new File("D:\\course\\01-开课");
File[] files = f.listFiles();
// foreach
for(File file : files){
//System.out.println(file.getAbsolutePath());
System.out.println(file.getName());
}
}
}

多线程

  • 什么是进程?什么是线程?

    进程是一个应用程序(1个进程是一个软件)。
    线程是一个进程中的执行场景/执行单元。
    一个进程可以启动多个线程。
    
  • 进程之间内存独立不共享,线程之间堆内存和方法区内存共享,栈内存独立,一个线程一个栈。

实现线程

java语言中,实现线程有三种方式:

第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。

1
2
3
4
5
6
7
8
9
10
// 定义线程类
public class MyThread extends Thread{
public void run(){

}
}
// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();

第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。

1
2
3
4
5
6
7
8
9
10
// 定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){

}
}
// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();

采用匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadTest04 {
public static void main(String[] args) {
// 创建线程对象,采用匿名内部类方式。
// 这是通过一个没有名字的类,new出来的对象。
Thread t = new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("t线程---> " + i);
}
}
});

// 启动线程
t.start();

for(int i = 0; i < 100; i++){
System.out.println("main线程---> " + i);
}
}
}

第二种方式:实现Callable接口。(JDK8新特性。)

优点:可以获取到线程的执行结果。
缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask; // JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。
public class ThreadTest15 {
public static void main(String[] args) throws Exception {
// 第一步:创建一个“未来任务类”对象。
// 参数非常重要,需要给一个Callable接口实现类对象。
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
// 线程执行一个任务,执行之后可能会有一个执行结果
// 模拟执行
System.out.println("call method begin");
Thread.sleep(1000 * 10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a + b; //自动装箱(300结果变成Integer)
}
});

// 创建线程对象
Thread t = new Thread(task);

// 启动线程
t.start();

// 这里是main方法,这是在主线程中。
// 在主线程中,怎么获取t线程的返回结果?
// get()方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println("线程执行结果:" + obj);

// main方法这里的程序要想执行必须等待get()方法的结束
// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
// 另一个线程执行是需要时间的。
System.out.println("hello world!");
}
}

线程对象的生命周期

​ 新建状态
​ 就绪状态
​ 运行状态
​ 阻塞状态
​ 死亡状态

线程生命周期

线程常用方法

  • 获取当前线程对象
1
Thread t = Thread.currentThread(); //返回值t就是当前线程。
  • 获取线程对象的名字
1
String name = 线程对象.getName();
  • 修改线程对象的名字
1
线程对象.setName("线程名字");//默认名字Thread-n
  • 线程休眠
1
Thread.sleep(long millis);

静态方法,作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。这行代码出现在A线程中,A线程就会进入休眠。

  • 中止线程睡眠
1
线程对象.interrupt();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
// 希望5秒之后,t线程醒来
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt();
}
}

class MyRunnable2 implements Runnable {

// 重点:run()当中的异常不能throws,只能try catch
// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
try {
// 睡眠1年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
// 打印异常信息
//e.printStackTrace();
}
//1年之后才会执行这里
System.out.println(Thread.currentThread().getName() + "---> end");

// 调用doOther
//doOther();
}

// 其它方法可以throws
/*public void doOther() throws Exception{

}*/
}
  • 终止线程执行(stop();已过时)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ThreadTest10 {
public static void main(String[] args) {
MyRunable4 r = new MyRunable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
// 模拟5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程
// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
r.run = false;
}
}
class MyRunable4 implements Runnable {
// 打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++){
if(run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// return就结束了,你在结束之前还有什么没保存的。
// 在这里可以保存呀。
//save....

//终止当前线程
return;
}
}
}
}

线程的调度

常见的线程调度模型?

  • 抢占式调度模型:

    线程的优先级越高,抢到的CPU时间片的概率就高一些/多一些。
    java采用的就是抢占式调度模型。
    
  • 均分式调度模型:

    平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
    有一些编程语言,采用这种方式。
    

线程调度方法(理解)

  • 线程优先级(实例)
1
2
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级

最低优先级1,默认优先级是5,最高优先级10

  • 线程让位(静态)
1
static void yield()

暂停当前正在执行的线程对象,并执行其他线程。yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。但在回到就绪之后,有可能还会再次抢到。

  • 合并线程(实例)
1
2
3
4
5
6
7
8
9
10
void join()  
合并线程
class MyThread1 extends Thread {
public void doSome(){
MyThread2 t = new MyThread2();
t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
}
}
class MyThread2 extends Thread{
}

线程安全

存在线程安全问题的三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。

解决线程安全问题:

使用“线程同步机制”让线程排队执行(不能并发)。

区分异步同步

  • 异步编程模型:

    ​ 线程各自执行各自的,谁也不需要等谁。
    ​ 其实就是:多线程并发(效率较高。)
    ​ 异步就是并发。

  • 同步编程模型:

    ​ 两个线程之间存在等待关系,效率较低。线程排队执行。
    ​ 同步就是排队。

synchronized三种写法

第一种:同步代码块

1
2
3
synchronized(线程共享对象){
同步代码块;
}

第二种:在实例方法上使用synchronized

    表示共享对象一定是this
    并且同步代码块是整个方法体。

第三种:在静态方法上使用synchronized

    表示找类锁。
    类锁永远只有1把。
    就算创建了100个对象,那类锁也只有一把。

变量线程安全

实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。

以上三大变量中:

  • 局部变量永远都不会存在线程安全问题。因为局部变量不共享(一个线程一个栈)。局部变量在栈中。所以局部变量永远都不会共享。所以使用局部变量时建议使用StringBuilder等非线程安全方法提高效率。

  • 实例变量在堆中,堆只有1个。静态变量在方法区中,方法区只有1个。
    堆和方法区都是多线程共享的,所以可能存在线程安全问题。

局部变量,常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。

死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){

}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){

}
}
}
}

守护线程

java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

注意:主线程main方法是一个用户线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run(){
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

定时器

定时器的作用:
间隔特定的时间,执行特定的程序。

在java中其实可以采用多种方式实现:

  • 使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行

        任务。这种方式是最原始的定时器。(比较low)
    
  • 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。

  • 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

wait和notify方法

  • wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。

  • wait方法和notify方法不通过线程对象调用:t.wait(),t.notify()..错误。

  • wait方法和notify方法建立在线程同步的基础之上。

  • wait()方法

1
2
Object o = new Object();
o.wait();

表示:
让正在o对象上活动的线程进入等待状态,直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上
活动的线程)”进入等待状态,并且释放掉线程之前占有的o对象的锁。

  • notify()方法
1
2
Object o = new Object();
o.notify();

表示:
唤醒正在o对象上等待的线程,不会释放o对象上之前占有的锁。

  • notifyAll()方法:唤醒o对象上处于等待的所有线程X。

使用wait方法和notify方法实现“生产者和消费者模式”

生产线程负责生产,消费线程负责消费。
生产线程和消费线程要达到均衡。
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/*模拟这样一个需求:
仓库我们采用List集合。
List集合中假设只能存储1个元素。
1个元素就表示仓库满了。
如果List集合中元素个数是0,就表示仓库空了。
保证List集合中永远都是最多存储1个元素。
必须做到这种效果:生产1个消费1个。
*/
import java.util.ArrayList;
import java.util.List;
public class ThreadTest16 {
public static void main(String[] args) {
// 创建1个仓库对象,共享的。
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){
// 给仓库对象list加锁。
synchronized (list){
if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
try {
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒消费者进行消费
list.notifyAll();
}
}
}
}
// 消费线程
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集合的锁
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序能够执行到此处说明仓库中有数据,进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者生产。
list.notifyAll();
}
}
}
}

反射机制

通过java语言中的反射机制可以操作字节码文件。可以读和修改字节码文件。通过反射机制可以操作代码片段。(class文件。)

反射机制的相关类在java.lang.reflect.*;包下。

获取类的字节码

三种方式:

1
2
3
第一种:Class c = Class.forName("完整类名带包名");
第二种:Class c = 对象.getClass();
第三种:Class c = 任何类型.class;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.Date;
public class ReflectTest01 {
public static void main(String[] args) {
/*
Class.forName()
1、静态方法
2、方法的参数是一个字符串。
3、字符串需要的是一个完整类名。
4、完整类名必须带有包名。java.lang包也不能省略。
*/
Class c1 = null;
Class c2 = null;
try {
c1 = Class.forName("java.lang.String"); // c1代表String.class文件,或者说c1代表String类型。
c2 = Class.forName("java.util.Date"); // c2代表Date类型
Class c3 = Class.forName("java.lang.Integer"); // c3代表Integer类型
Class c4 = Class.forName("java.lang.System"); // c4代表System类型
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// java中任何一个对象都有一个方法:getClass()
String s = "abc";
Class x = s.getClass(); // x代表String.class字节码文件,x代表String类型。
System.out.println(c1 == x); // true(==判断的是对象的内存地址。)

Date time = new Date();
Class y = time.getClass();
System.out.println(c2 == y); // true (c2和y两个变量中保存的内存地址都是一样的,都指向方法区中的字节码文件。)
// 第三种方式,java语言中任何一种类型,包括基本数据类型,它都有.class属性。
Class z = String.class; // z代表String类型
Class k = Date.class; // k代表Date类型
Class f = int.class; // f代表int类型
Class e = double.class; // e代表double类型
System.out.println(x == z); // true
}
}

通过Class的newInstance()方法来实例化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
获取到Class,能干什么?
通过Class的newInstance()方法来实例化对象。
注意:newInstance()方法内部实际上调用了无参数构造方法,必须保证无参构造存在才可以。
*/
public class ReflectTest02 {
public static void main(String[] args) {
// 这是不使用反射机制,创建对象
User user = new User();
System.out.println(user);
// 下面这段代码是以反射机制的方式创建对象。
try {
// 通过反射机制,获取Class,通过Class来实例化对象
Class c = Class.forName("com.bjpowernode.java.bean.User"); // c代表User类型。
// newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
// 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
Object obj = c.newInstance();
System.out.println(obj); // com.bjpowernode.java.bean.User@10f87f48
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
public class User {
public User(){
System.out.println("无参数构造方法!");
}
// 定义了有参数的构造方法,无参数构造方法就没了。
public User(String s){
}
}

验证反射机制的灵活性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.FileReader;
import java.util.Properties;
/*
验证反射机制的灵活性。
java代码写一遍,再不改变java源代码的基础之上,可以做到不同对象的实例化。
非常之灵活。(符合OCP开闭原则:对扩展开放,对修改关闭。)
*/
public class ReflectTest03 {
public static void main(String[] args) throws Exception{
// 这种方式代码就写死了。只能创建一个User类型的对象
//User user = new User();
// 以下代码是灵活的,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建出不同的实例对象。
// 通过IO流读取classinfo.properties文件
FileReader reader = new FileReader("chapter/classinfo2.properties");
// 创建属性类对象Map
Properties pro = new Properties(); // key value都是String
// 加载
pro.load(reader);
// 关闭流
reader.close();
// 通过key获取value
String className = pro.getProperty("className");
//System.out.println(className);
// 通过反射机制实例化对象
Class c = Class.forName(className);
Object obj = c.newInstance();
System.out.println(obj);
}
}

只加载静态代码块

记住,重点:
如果只希望一个类的静态代码块执行,其它代码一律不执行,可以使用:

1
Class.forName("完整类名");

​ 这个方法的执行会导致类加载,类加载时,静态代码块执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReflectTest04 {
public static void main(String[] args) {
try {
// Class.forName()这个方法的执行会导致:类加载。
Class.forName("com.java.reflect.MyClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public class MyClass {
// 静态代码块在类加载时执行,并且只执行一次。
static {
System.out.println("MyClass类的静态代码块执行了!");
}
}

获取文件绝对路径

以下的方式是通用的。但前提是:文件需要在类路径下。

1
2
String Thread.currentThread().getContextClassLoader()
.getResource("文件名").getPath();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.FileReader;
public class AboutPath {
public static void main(String[] args) throws Exception{
// 这种方式的路径缺点是:移植性差,在IDEA中默认的当前路径是project的根。
// 这个代码假设离开了IDEA,换到了其它位置,可能当前路径就不是project的根了,这时这个路径就无效了。
//FileReader reader = new FileReader("chapter25/classinfo2.properties");
// 接下来说一种比较通用的一种路径。即使代码换位置了,这样编写仍然是通用的。
// 注意:使用以下通用方式的前提是:这个文件必须在类路径下。
// 什么类路径下?方式在src下的都是类路径下。【记住它】
// src是类的根路径。
/*
解释:
Thread.currentThread() 当前线程对象
getContextClassLoader() 是线程对象的方法,可以获取到当前线程的类加载器对象。
getResource() 【获取资源】这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源。
*/
String path = Thread.currentThread().getContextClassLoader()
.getResource("classinfo2.properties").getPath(); // 这种方式获取文件绝对路径是通用的。

// 采用以上的代码可以拿到一个文件的绝对路径。
// /C:/Users/Administrator/IdeaProjects/javase/out/production/chapter25/classinfo2.properties
System.out.println(path);
// 获取db.properties文件的绝对路径(从类的根路径下作为起点开始)
String path2 = Thread.currentThread().getContextClassLoader()
.getResource("com/java/bean/db.properties").getPath();
System.out.println(path2);

}
}

直接以流的形式返回:

1
2
InputStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("文件名");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileReader;
import java.io.InputStream;
import java.util.Properties;
public class IoPropertiesTest {
public static void main(String[] args) throws Exception{
// 获取一个文件的绝对路径了!!!!!
/*String path = Thread.currentThread().getContextClassLoader()
.getResource("classinfo2.properties").getPath();
FileReader reader = new FileReader(path);*/
// 直接以流的形式返回。
InputStream reader = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("classinfo2.properties");

Properties pro = new Properties();
pro.load(reader);
reader.close();
// 通过key获取value
String className = pro.getProperty("className");
System.out.println(className);
}
}

类 Field

怎么通过反射机制访问一个java对象的属性?
给属性赋值set
获取属性的值get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.lang.reflect.Field;
public class ReflectTest07 {
public static void main(String[] args) throws Exception{
// 我们不使用反射机制,怎么去访问一个对象的属性呢?
Student s = new Student();
// 给属性赋值
s.no = 1111; //三要素:给s对象的no属性赋值1111
//要素1:对象s
//要素2:no属性
//要素3:1111
// 读属性值
// 两个要素:获取s对象的no属性的值。
System.out.println(s.no);

// 使用反射机制,怎么去访问一个对象的属性。(set get)
Class studentClass = Class.forName("com.java.bean.Student");
Object obj = studentClass.newInstance(); // obj就是Student对象。(底层调用无参数构造方法)
// 获取no属性(根据属性的名称来获取Field)
Field noFiled = studentClass.getDeclaredField("no");
// 给obj对象(Student对象)的no属性赋值
/*
虽然使用了反射机制,但是三要素还是缺一不可:
要素1:obj对象
要素2:no属性
要素3:2222值
注意:反射机制让代码复杂了,但是为了一个“灵活”,这也是值得的。
*/
noFiled.set(obj, 22222); // 给obj对象的no属性赋值2222

// 读取属性的值
// 两个要素:获取obj对象的no属性的值。
System.out.println(noFiled.get(obj));

// 可以访问私有的属性吗?
Field nameField = studentClass.getDeclaredField("name");

// 打破封装(反射机制的缺点:打破封装,不安全)
// 这样设置完之后,在外部也是可以访问private的。
nameField.setAccessible(true);

// 给name属性赋值
nameField.set(obj, "jackson");
// 获取name属性的值
System.out.println(nameField.get(obj));
}
}
public class Student {
// Field翻译为字段,其实就是属性/成员
// 4个Field,分别采用了不同的访问控制权限修饰符
private String name; // Field对象
protected int age; // Field对象
boolean sex;
public int no;
public static final double MATH_PI = 3.1415926;
}

类 Method

可变长度参数

int… args 这就是可变长度参数
语法是:类型… (注意:一定是3个点。)

  • 可变长度参数要求的参数个数是:0~N个。
  • 可变长度参数在参数列表中必须在最后一个位置上,而且可变长度参数只能有1个。
  • 可变长度参数可以当做一个数组来看待。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ArgsTest {
public static void main(String[] args) {
m();
m(10);
m(10, 20);
// 编译报错
//m("abc");
m2(100);
m2(200, "abc");
m2(200, "abc", "def");
m2(200, "abc", "def", "xyz");
m3("ab", "de", "kk", "ff");
String[] strs = {"a","b","c"};
// 也可以传1个数组
m3(strs);
// 直接传1个数组
m3(new String[]{"我","是","中","国", "人"}); //没必要

m3("我","是","中","国", "人");
}
public static void m(int... args){
System.out.println("m方法执行了!");
}
//public static void m2(int... args2, String... args1){}
// 必须在最后,只能有1个。
public static void m2(int a, String... args1){

}
public static void m3(String... args){
//args有length属性,说明args是一个数组!
// 可以将可变长度参数当做一个数组来看。
for(int i = 0; i < args.length; i++){
System.out.println(args[i]);
}
}
}

通过反射机制调用对象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.lang.reflect.Method;
public class ReflectTest10 {
public static void main(String[] args) throws Exception{
// 不使用反射机制,怎么调用方法
// 创建对象
UserService userService = new UserService();
// 调用方法
/*
要素分析:
要素1:对象userService
要素2:login方法名
要素3:实参列表
要素4:返回值
*/
boolean loginSuccess = userService.login("admin","123");
//System.out.println(loginSuccess);
System.out.println(loginSuccess ? "登录成功" : "登录失败");
// 使用反射机制来调用一个对象的方法该怎么做?
Class userServiceClass = Class.forName("com.bjpowernode.java.service.UserService");
// 创建对象
Object obj = userServiceClass.newInstance();
// 获取Method
Method loginMethod = userServiceClass.getDeclaredMethod("login", String.class, String.class);
//区别同名方法:Method loginMethod = userServiceClass.getDeclaredMethod("login", int.class);
// 调用方法
// 调用方法有几个要素? 也需要4要素。
/*
四要素:
loginMethod方法
obj对象
"admin","123" 实参
retValue 返回值
*/
// 反射机制中最最最最最重要的一个方法。
Object retValue = loginMethod.invoke(obj, "admin","123123");
System.out.println(retValue);
}
}
/**
* 用户业务类
*/
public class UserService {
/**
* 登录方法
* @param name 用户名
* @param password 密码
* @return true表示登录成功,false表示登录失败!
*/
public boolean login(String name,String password){
if("admin".equals(name) && "123".equals(password)){
return true;
}
return false;
}
// 可能还有一个同名login方法
// java中怎么区分一个方法,依靠方法名和参数列表。
public void login(int i){
}
/**
* 退出系统的方法
*/
public void logout(){
System.out.println("系统已经安全退出!");
}
}

类 Constructor

通过反射机制调用构造方法实例化java对象(非重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.lang.reflect.Constructor;
public class ReflectTest12 {
public static void main(String[] args) throws Exception{
// 不使用反射机制怎么创建对象
Vip v1 = new Vip();
Vip v2 = new Vip(110, "zhangsan", "2001-10-11", true);
// 使用反射机制怎么创建对象
Class c = Class.forName("com.bjpowernode.java.bean.Vip");
// 调用无参数构造方法
Object obj = c.newInstance();
System.out.println(obj);
// 调用有参数的构造方法怎么办?
// 第一步:先获取到这个有参数的构造方法
Constructor con = c.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);
// 第二步:调用构造方法new对象
Object newObj = con.newInstance(110, "jackson", "1990-10-11", true);
System.out.println(newObj);
// 获取无参数构造方法
Constructor con2 = c.getDeclaredConstructor();
Object newObj2 = con2.newInstance();
System.out.println(newObj2);
}
}
public class Vip {
int no;
String name;
String birth;
boolean sex;
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 birth) {
this.no = no;
this.name = name;
this.birth = birth;
}

public Vip(int no, String name, String birth, boolean sex) {
this.no = no;
this.name = name;
this.birth = birth;
this.sex = sex;
}

@Override
public String toString() {
return "Vip{" +
"no=" + no +
", name='" + name + '\'' +
", birth='" + birth + '\'' +
", sex=" + sex +
'}';
}
}

获取父类实现的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ReflectTest13 {
public static void main(String[] args) throws Exception{
// String举例
Class stringClass = Class.forName("java.lang.String");
// 获取String的父类
Class superClass = stringClass.getSuperclass();
System.out.println(superClass.getName());
// 获取String类实现的所有接口(一个类可以实现多个接口。)
Class[] interfaces = stringClass.getInterfaces();
for(Class in : interfaces){
System.out.println(in.getName());
}
}
}

注解Annotation

注解Annotation是一种引用数据类型。编译之后也是生成xxx.class文件。

自定义注解:

1
2
[修饰符列表] @interface 注解类型名{
}

使用注解语法格式

1
@注解类型名

注解可以出现在类上、属性上、方法上、变量上等….还可以出现在注解类型上。

  • 注解当中有属性,那么必须给属性赋值。(除非该属性使用default指定了默认值。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyAnnotationTest {
// 报错的原因:如果一个注解当中有属性,那么必须给属性赋值。(除非该属性使用default指定了默认值。)
/*@MyAnnotation
public void doSome(){
}*/
//@MyAnnotation(属性名=属性值,属性名=属性值,属性名=属性值)
//指定name属性的值就好了。
@MyAnnotation(name = "zhangsan", color = "红色")
public void doSome(){
}
}
public @interface MyAnnotation {
/**
* 我们通常在注解当中可以定义属性,以下这个是MyAnnotation的name属性。
* 看着像1个方法,但实际上我们称之为属性name。
* @return
*/
String name();
String color();
int age() default 25; //属性指定默认值
}
  • 如果一个注解的属性的名字是value,并且只有一个属性的话,在使用的时,该属性名可以省略。
1
2
3
4
5
6
7
8
9
10
11
public class MyAnnotationTest {
@MyAnnotation(value = "haha")
public void doSome(){
}
@MyAnnotation("haha")
public void doOther(){
}
}
public @interface MyAnnotation {
String value();
}
  • 注解当中属性的类型可以是:byte short int long float double boolean char String Class 枚举类型以及以上每一种的数组形式。
  • 反射注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class ReflectAnnotationTest {
public static void main(String[] args) throws Exception{
// 获取这个类
Class c = Class.forName("com.java.annotation5.MyAnnotationTest");
// 判断类上面是否有@MyAnnotation
//System.out.println(c.isAnnotationPresent(MyAnnotation.class)); // true
if(c.isAnnotationPresent(MyAnnotation.class)){
// 获取该注解对象
MyAnnotation myAnnotation = (MyAnnotation)c.getAnnotation(MyAnnotation.class);
//System.out.println("类上面的注解对象" + myAnnotation); // @com.bjpowernode.java.annotation5.MyAnnotation()
// 获取注解对象的属性怎么办?和调接口没区别。
String value = myAnnotation.value();
System.out.println(value);
}
// 判断String类上面是否存在这个注解
Class stringClass = Class.forName("java.lang.String");
System.out.println(stringClass.isAnnotationPresent(MyAnnotation.class)); // false
}
}
//==================================================================
//只允许该注解可以标注类、方法
@Target({ElementType.TYPE, ElementType.METHOD})
// 希望这个注解可以被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "北京大兴区";
}
//==================================================================
@MyAnnotation("上海浦东区")
public class MyAnnotationTest {
//@MyAnnotation
int i;
//@MyAnnotation
public MyAnnotationTest(){
}
@MyAnnotation
public void doSome(){
//@MyAnnotation
int i;
}
}

需要掌握的JDK内置注解:

java.lang包下的注释类型:

  • Deprecated:用 @Deprecated 注释的程序元素,表示不鼓励程序员使用这样的元素,通常是因为它很危险或存在更好的选择。

  • Override 表示一个方法声明打算重写超类中的另一个方法声明。
    编译器看到方法上有这个注解的时候,编译器会自动检查该方法是否重写了父类的方法。
    如果没有重写,报错。只在编译阶段起作用,和运行期无关。

不用掌握:
​ SuppressWarnings 指示应该在注释元素(以及包含在该注释元素中的所有程序元素)中取消显示指定的编译器警告。

元注解

用来标注“注解类型”的“注解”,称为元注解。

常见的元注解:
Target
Retention

关于Target注解:

​ 这是一个元注解,用来标注“注解类型”的“注解”
​ 这个Target注解用来标注“被标注的注解”可以出现在哪些位置上。

1
2
3
4
5
6
7
8
9
@Target(ElementType.METHOD)//表示“被标注的注解”只能出现在方法上。
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
/*表示该注解可以出现在:
构造方法上
字段上
局部变量上
方法上
....
类上...*/

关于Retention注解:

​ 这是一个元注解,用来标注“注解类型”的“注解”
​ 这个Retention注解用来标注“被标注的注解”最终保存在哪里。

1
2
3
@Retention(RetentionPolicy.SOURCE)//表示该注解只被保留在java源文件中。
@Retention(RetentionPolicy.CLASS)//表示该注解被保存在class文件中。
@Retention(RetentionPolicy.RUNTIME)//表示该注解被保存在class文件中,并且可以被反射机制所读取。
Retention的源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
	//元注解	
public @interface Retention {
//属性
RetentionPolicy value();
}
RetentionPolicy的源代码:
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
//@Retention(value=RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation{}