文章目录
JavaSE
1. Java语言概述
1.1 Java语言运行机制和运行过程
Java语言的特点
1.面向对象
两个基本概念: 类、对象
三大特性: 封装、继承、多态
2.健壮性
没指针、内存申请释放(gc)等
3.跨平台
Write once,Run anywhere
JVM 运行 -> 可在不同操作系统上运行
gc
1.不再使用的内存空间应回收 —— 垃圾回收
C/C++ 需要程序员去回收无用内存
Java 提供一种系统级线程跟踪存储空间的分配情况. 并且在 JVM 空闲时,检查并释放那些可释放的存储空间.
2.gc 自动进行,程序猿无法精准控制和干预
3.Java 程序是会出现内存泄漏和内存溢出问题的
eg.StackOverflowException
1.2 JDK、JRE、JVM 的关系
JDK = JRE + 开发工具集(eg.Javac 编译工具等)
JRE = JVM + JavaSE 标准类库
1.3 HelloWorld.java
public class HelloWorld {
public static void main (String[] args) {
System.out.println("Hello World!");
}
}
// 源文件(.java)-- 编译(javac.exe) --> 字节码文件(.class) -- 运行(java.exe) --> 结果
2. Java 基本语法
2.1 关键字和保留字
2.2 标识符
1.标识符 :
Java 对各种变量、方法和类等要素命名时使用的字符序列
2. 合法标识符规则 :
由 26 个英文字母大小写,0 ~ 9,_ ,$ 组成
不可以数字开头
不能使用关键字和保留字,但能包含
严格区分大小写,长度不限
不能包含空格
【PS】: 不遵守 -> 编译不过
Java 命名规范 :
包名 : 小写
类、接口 : 大驼峰
变量、方法 : 小驼峰
常量 : XXX_YYY_ZZZ
2.3 变量
2.3.1 分类
变量 :
1. 内存中的一个存储区域
2. 该区域的数据可以在同一类型的范围内不断变化
3. 变量是程序中最基本的存储单元,包含变量类型、变量名、变量值
作用 :
用于在内存中存储数据
注意点 :
1. Java 中每个变量必须先声明后使用
2. 使用变量名来访问这一块的数据
3. 变量的作用域在其所定义的 {} 中
4. 变量只有在其作用域中才有效
5. 同一个作用域内,不能定义重名的变量
变量类型 :
1. 基本数据类型 : byte / short / int / long / float / double / char / boolean
2. 引用数据类型 : 类 / 接口 / 数组 / 枚举 / 注解 / 记录
2.3.2 整型
2.3.3 浮点型
2.3.4 char 型
2.3.5 布尔型
字符类型: boolean
-> true & false
内存层面 :
oracle : 布尔数据类型只有两个可能的值:真和假。使⽤此数据类型为跟踪真/假条件的简单标记。这种数据类型就表示这⼀点信息,但是它的“⼤⼩”并不是精确定义的.
《Java虚拟机规范》:“虽然定义了boolean这种数据类型,但是只对它提供了⾮常有限的⽀持。在Java虚拟机中没有任何供boolean值专⽤的字节码指令,Java语⾔表达式所操作的boolean值,在编译之后都使⽤Java虚拟机中的int数据类型来代替,⽽boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位”.
这样我们可以得出 boolean 类型占了单独使⽤是 4 个字节,在数组中⼜是 1 个字节。
2.3.6 字符编码
2.3.7 自动类型转换
1. 容量小的类型自动转换为容量大的数据类型 :
byte / char / short ---> int ---> long ---> float ---> double
2. 有多种类型的数据混合运算时,系统会首先自动将所有数据转换为容量大的数据类型,然后再进行计算...
3. byte / char / short 之间不会相互转换,它们仨在计算时首先转换为 int 类型.
4. boolean 类型不能与其它数据类型运算.
5. 任何基本数据类型的值和字符串进行 '+' 运算时,基本数据类型的值将自动转换为 String 类型.
2.3.8 强制类型转换
eg. int a = (int) 12.3;
1. 可能会造成精度损失
2. 通常字符串不能直接转换为基本类型,但可以通过基本类型对应的包装类实现把字符串转换为基本类型
eg. String str = "1";
int x = Integer.parseInt(str);
【PS】: boolean 类型不能转化为其他类型...
2.3.9 特殊情况
long a = 12121211212121212; // 编译失败,默认按 int 存储,结尾需加 'l' / L'
float b = 12.12; // 编译失败,结尾需加 'f' / 'F'
2.3.10 String
1. String 是引用数据类型...
2. 两个小练习 (注意自动类型提升)
eg.1.
char c = 'a'; // a
int num = 10;
String str = "nuo";
System.out.println(c + num + str); // 107nuo
System.out.println(c + str + num); // anuo10
System.out.println(c + (num + str)); // a10nuo
System.out.println((c + num) + str); // 107nuo
System.out.println(str + num + c); // nuo10a
eg.2.
System.out.println("* *"); // * *
System.out.println('*' + '\t' + '*'); // 93
System.out.println('*' + "\t" + '*'); // * *
System.out.println('*' + '\t' + "*"); // 51*
System.out.println('*' + ('\t' + "*")); // * *
2.3.11 进制
二进制 :第一位为符号位
0 -> 正数
1 -> 负数
1. 正数 :原码、反码、补码相同
eg. 0110
1 * 2^2 + 1 * 2^1 + 0 * 2^0
2. 负数 :
原码 :最高位(符号位):1
反码 :除符号位外,其余位取反
补码 :反码 + 1
【PS】:计算机底层都以二进制补码的方式存储整数.
3. 十进制 -> 二进制
num 除 2 取余的逆
2.4 运算符
2.4.1 算术运算符
1. 最终结果与被模数的符号相同 :
sout(12 % 5); // 2
sout((-12) % 5); // -2
sout(12 % (-5)); // 2
sout((-12) % (-5)); // -2
2. 自增 1 不会改变本身的数据类型
short x1 = 1;
x1 ++; // short
short x2 = 1;
x2 = x2 + 1; // 编译失败 (自动类型提升为 int,无法赋值给 short 类型)
x2 += 1; // 正确
x2 = (short)(x2 + 1); // 正确
【PS】: '++' or '+=' 不会改变自身的数据类型
2.4.2 赋值运算符
'=' :
1. 两边数据类型不同时,可采用自动类型转换 or 强制类型转换处理。
2. 支持连续赋值。
扩展 :
'+=' '-=' '*=' '/=' '%='
2.4.3 比较运算符(关系运算符)
2.4.4 逻辑运算符
区分 :
'|' 和 '||'
'&' 和 '&&'
2.4.5 位运算符
2.4.6 三元运算符
? : ;
2.4.7 运算符优先级
2.5 程序流程控制
2.5.1 顺序
2.5.2 分支
1. if-else
2. switch-case
switch(表达式)中,表达式类型 :
byte / short / char / int / 枚举 / String
2.5.3 循环
1. while
2. do...while
3. for
2.6 随机数
生成随机数 :
Math.random() => [0.0,1.0)
注意最后取
int =>(int) (Math.random()=>[0.0,1.0) )
or
double
题目要求产生的随机数如果为[a,b),则使用(int)(Math.random() * (b - a) + a)
2.7 读取用户输入的 char 型数据
字符串的读取 & char的使用
【PS】:char不可直接使用char ch = scan.nextChar(),没这个方法咳咳(类似数组,从0开始)
2.8 练习
2.8.1 判断天数
import java.util.Scanner;
//输入2019年的 month & day ,判断这是2019年的第几天
//倒序书写case ,不标 break ,进而达到累加效果【也可使用if-else,但是冗余,出现三角排布】
public class Demo_01 {
public static void main(String[] args) {
Scanner num = new Scanner(System.in);
System.out.print("请输入2019的 Month :");
int Month = num.nextInt();
System.out.print("请输入2019的 Day :");
int Day = num.nextInt();
int sum = 0;
switch (Month) {
case 12:
sum += 30;
case 11:
sum += 31;
case 10:
sum += 30;
case 9:
sum += 31;
case 8:
sum += 31;
case 7:
sum += 30;
case 6:
sum += 31;
case 5:
sum += 30;
case 4:
sum += 31;
case 3:
sum += 28;
case 2:
sum += 31;
case 1:
sum += Day;
}
System.out.println(Month + "月" + Day + "日" + "为2019年的第" + sum + "天");
}
}
2.8.2 素数
import java.util.Scanner;
//类比C语言,根据 t 的取值来判断一个数是否为素数
//输出 x 以内所有的素数
public class Demo_02 {
public static void main(String[] args) {
Scanner num = new Scanner(System.in);
int x = num.nextInt(); num.close();
int cnt = 0;
for (int i = 2; i <= x; i++) {
int t = 1;
for (int j = 2; j < i; j++) {
if (i % j == 0) {
t = 0;
}
}
if (t == 1) {
System.out.print(i + "\t");
cnt++;
if (cnt % 5 == 0) {
System.out.println();
}
}
}
}
}
import java.util.Scanner;
//三次优化,减少运行所需时间
//增加了计时器,便于比较【计时器的调用】
//类比C语言,根据 t 的取值来判断一个数是否为素数
//输出 x 以内所有的素数
public class Demo_02 {
public static void main(String[] args) {
// 获取当前时间据 1970- 01 - 01 00:00:00 的毫秒数
Long start = System.currentTimeMillis();
Scanner num = new Scanner(System.in);
System.out.println("你想输出前 ?个整数中的素数:");
int x = num.nextInt();
num.close();
// 调试:
// int x = 100;
int cnt = 0;
for (int i = 2; i <= x; i++) {
int t = 1;
//优化 1
if (i > 2 && i % 2 == 0) {
//优化 2
continue;
}
for (int j = 2; j <= Math.sqrt(i); j++) {
if (i % j == 0) {
t = 0;
//优化 3
break;
}
}
if (t == 1) {
System.out.print(i + "\t");
cnt++;
if (cnt % 5 == 0) {
System.out.println();
}
}
}
Long end = System.currentTimeMillis();
System.out.println("耗时(ms):" + (end - start));
}
}
2.8.3 衡量一个功能代码的优劣
- 正确性
- 可读性
- 健壮性(完备)
- 高效率 && 低存储:时间复杂度 && 空间复杂度 【衡量算法的好坏】
3. 数组
3.1 一维数组
3.1.1 base
package com.nuo.Demo;
//初试 Array =>一维
/**
* 数组 =>Array => 引用数据类型的变量 && 长度一旦确定则无法修改
* 一维数组 二维数组 三维数组 ......
* 数组的元素类型可分为:
* 1.基本数据类型元素的数组
* 2.引用数据类型元素的数组 <=> 对象数组 【连续空间存储】
* 1.数组名
* 2.元素
* 3.角标 (索引)
* 4.数组长度
* Array:
* 1.数组的声明和初始化
* 2.如何获取数组长度
* 3.如何遍历数组
* 4.数组的默认初始化值
* int => 0
* float => 0.0
* double => 0.0
* char => '\u0000' or ' ' or 0 (PS: 非 '0')
* String => null
* boolean => false
* 5.数组的内存解析
*/
public class Demo_03 {
public static void main(String[] args) {
System.out.println("数组的声明 && 初始化 : ");
System.out.println("=======================================================");
// 1.1:静态初始化 : 数组的初始化和数组元素的赋值操作同时进行
int[] nums = new int[]{1, 2, 3, 4};
int ids [];
ids = new int[4];
// 1.2:动态初始化 : 数组的初始化和数组元素的赋值操作分开进行
String[] names = new String[5];
// 5 => 数组长度
// 错误写法 :
// int [] x = new int[];
// int [3] y = new int[];
// 初始化动态数组 :
names[0] = "小诺";
names[1] = "小诺";
names[2] = "小诺";
names[3] = "小诺";
names[4] = "小诺";
// 遍历
System.out.println("数组的遍历 : ");
System.out.println("=======================================================");
for (int i = 0; i < 5; i++) {
System.out.print(names[i] + "\t");
}
System.out.println();
// 获取数组长度
// 属性 : length
System.out.println("nums.length = " + nums.length);
System.out.println("names.length = " + names.length);
System.out.println("=======================================================");
// 数组的默认初始化值
System.out.println("数组的默认初始化值 1.1 【int】 :");
int[] x = new int[5];
for (int i = 0; i < 5; i++) {
System.out.println("x[" + i + "]=" + x[i]);
}
// int => 0
System.out.println("=======================================================");
System.out.println("数组的默认初始化值 1.2 【float】 :");
float[] y = new float[5];
for (int i = 0; i < 5; i++) {
System.out.println("y[" + i + "]=" + y[i]);
}
// float => 0.0
System.out.println("=======================================================");
System.out.println("数组的默认初始化值 1.3 【double】 :");
double[] h = new double[5];
for (int i = 0; i < 5; i++) {
System.out.println("h[" + i + "]=" + h[i]);
}
// double => null
System.out.println("=======================================================");
System.out.println("数组的默认初始化值 1.4 【char】 :");
char[] g = new char[5];
for (int i = 0; i < 5; i++) {
System.out.println("g[" + i + "]=" + g[i]);
}
// char => 0 <=> ' ' (PS: 非 '0')
System.out.println("=======================================================");
System.out.println("数组的默认初始化值 1.5 【string】 :");
String[] z = new String[5];
for (int i = 0; i < 5; i++) {
System.out.println("z[" + i + "]=" + z[i]);
}
// String => null
System.out.println("=======================================================");
System.out.println("数组的默认初始化值 1.6 【boolean】 :");
boolean[] f = new boolean[5];
for (int i = 0; i < 5; i++) {
System.out.println("f[" + i + "]=" + f[i]);
}
// boolean => false
}
}
3.1.2 内存解析
3.2 二维数组
3.2.1 base
package com.nuo.Demo;
// 初试 Array => 二维 => 可拓展
/**
* Array:
* 1.数组的声明和初始化
* 2.如何获取数组长度 <=> 一维
* 3.如何遍历数组
* 4.数组的默认初始化值
* 4.1 int arr [][] = new int[2][2];
* arr [0] -> 地址值
* arr [0][0] -> 同一维数组
* 4.2 int arr [][] = new int[2][];
* arr [0] -> null
* arr [0][0] -> 不能调用,否则报错
* 5.数组的内存解析
*/
public class Demo_04 {
public static void main(String[] args) {
System.out.println("数组的声明 && 初始化 : ");
// 1.静态声明
int nums_1 [][] = new int[][] {{1,2,3},{4,5},{6,7,8}};
int nums_2 [][] = {{1,2,3},{4,5},{6,7,8}};
String str [][] = new String[][]{{"小诺"},{"小","诺"}};
// 2.动态声明
int nums_3 [][] = new int[2][2];
//[]位置可更换,参照一维数组
int nums_4 [][] = new int[2][ ] ;
System.out.println("**********************************************************");
System.out.println("PS : ");
System.out.println(nums_4[1]);
// =>空指针 => null
// System.out.println(nums_4[1][0]);
// =>非法访问空指针 => 编译失败
System.out.println("**********************************************************");
// 错误写法
// int nums_ [2][2] = new int[][];
// int nums_ [][] = new int[2][2] ;
// int nums_ [][] = new int[2][2]{{1,2},{4,5}};
System.out.println("=======================================================");
System.out.println("数组的长度 : ");
System.out.println("num_1.length = " + nums_1.length); // => 3
System.out.println("num_1[].length = " + nums_1[1].length); // => 2
System.out.println("=======================================================");
System.out.println("数组的遍历 : ");
for (int i = 0; i < nums_1.length; i++) {
for (int j = 0; j < nums_1[i].length; j++) {
System.out.println( "nums_1["+ i +"]["+ j + "] = " + nums_1[i][j]);
}
}
// 非法访问空指针 => 编译失败
// System.out.println(nums_1[4][5]);
/**Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
* at com.nuo.Demo.Demo_19.main(Demo_19.java:44)
*/
System.out.println("=======================================================");
System.out.println("数组的使用 : ");
System.out.println(nums_1); // => [[I@135fbaa4
System.out.println(nums_1[1]); // => [I@45ee12a7
System.out.println(nums_1[1][1]); // => 5
System.out.println(str); // => [[Ljava.lang.String;@330bedb4
System.out.println(str [1]); // => [Ljava.lang.String;@2503dbd3
System.out.println(str [0][0]); // => 小诺
// '['的数量 <=> 对应几维数组
// 'I' <=> 对应数组类型
// '@' <=> 后面+地址
// '135fbaa4' <=> 0x地址位置
System.out.println("=======================================================");
}
}
3.2.2 内存解析
3.3 数组的 copy & reverse
array 2 = array 1 <=> 快捷打开方式,改变array 2 ,实际array 1 会进行同样的改变 .
package com.nuo.Demo;
//Array & copy
public class Demo_05 {
public static void main(String[] args) {
int[] arr1, arr2, arr3;
arr1 = new int[]{1, 2, 3};
// 正确的 copy (for 遍历赋值):
arr2 = new int[arr1.length];
System.out.println("arr1 : ");
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + "\t");
}
System.out.println();
System.out.println("arr2 : ");
for (int i = 0; i < arr1.length; i++) {
arr2[i] = arr1[i];
System.out.print(arr2[i] + "\t");
}
System.out.println();
System.out.println("arr2 改版 : ");
for (int i = 0; i < arr2.length; i++) {
if (i % 2 == 0) {
arr2[i] = i;
}
}
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr2[i] + "\t");
}
System.out.println();
System.out.println("arr1 : ");
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + "\t");
}
System.out.println();
System.out.println("*********************************************");
// 错误的赋值 :
arr3 = arr1;
for (int i = 0; i < arr3.length; i++) {
if (i % 2 == 0) {
arr3[i] = i;
}
}
System.out.println("arr3 改版 : ");
for (int i = 0; i < arr3.length; i++) {
System.out.print(arr3[i] + "\t");
}
System.out.println();
System.out.println("arr1 : ");
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + "\t");
}
System.out.println();
}
}
package com.nuo.Demo;
import java.util.Arrays;
//数组的反转
public class Demo_06 {
public static void main(String[] args) {
int[] arr1; arr1 = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println("arr1 : ");
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + "\t");
}
System.out.println();
for (int i = 0; i < arr1.length / 2; i++) {
int t = arr1[i];
arr1[i] = arr1[arr1.length - i - 1];
arr1[arr1.length - i - 1] = t;
}
System.out.println("arr1 改版 : ");
// System.out.println(Arrays.toString(arr1));
// => 遍历
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + "\t");
}
System.out.println();
}
}
3.4 练习
3.4.1 杨辉三角
package com.nuo.Demo;
import java.util.Scanner;
// Array => 万恶杨辉三角
/**
* 1
* 1 1
* 1 2 1
* 1 3 3 1
*/
public class Demo_07 {
public static void main(String[] args) {
Scanner x = new Scanner(System.in);
int num = x.nextInt();
x.close();
int n [][] = new int[num][num];
// n[0][0] = 1 ;
// System.out.println();
for (int i = 0; i < num ; i++) {
n[i][0] = 1 ;
System.out.print(n[i][0] + "\t");
for (int j = 1; j <=i ; j++) {
n[i][j] = n[i-1][j-1] + n[i-1][j];
System.out.print(n[i][j] + "\t");
}
System.out.println();
}
}
}
3.4.2 万恶回形数
package com.nuo.Demo;
import java.util.Scanner;
//回形数
/**
* 1 2 3 4 5
* <p>
* 16 17 18 19 6
* <p>
* 15 24 25 20 7
* <p>
* 14 23 22 21 8
* <p>
* 13 12 11 10 9
*
*/
public class Demo_08 {
public static void main(String[] args) {
Scanner x = new Scanner(System.in);
System.out.print("第一行你想要?个数 : ");
int num = x.nextInt();
x.close();
int[][] arr = new int[num][num];
int h = 1;
for (int cnt = 0; cnt < arr.length / 2 ; cnt++) {
//上行
// System.out.println("上行");
for (int i = cnt; i < arr.length - cnt - 1; i++, h++) {
arr[cnt][i] = h;
// System.out.println( cnt +"\t"+ i +"\t"+arr[cnt][i]+"\t");
}
//System.out.println("*********************************************");
//右列
// System.out.println("右列");
for (int i = cnt; i < arr.length - cnt - 1; i++, h++) {
arr[i][arr.length - cnt - 1] = h;
// System.out.println(i +"\t"+ (arr.length - cnt - 1 )+"\t"+arr[i] [arr.length - cnt - 1]+"\t");
}
//System.out.println("*********************************************");
//下行
// System.out.println("下行");
for (int i = arr.length - cnt - 1; i > cnt; i--, h++) {
arr[arr.length - cnt - 1][i] = h;
// System.out.println((arr.length - cnt - 1)+"\t" + i +"\t"+arr[arr.length - cnt - 1][i]+"\t");
}
//System.out.println("*********************************************");
//左列
// System.out.println("左列");
for (int i = arr.length - cnt - 1; i > cnt; i--, h++) {
arr[i][cnt] = h;
// System.out.println(i +"\t"+ cnt +"\t" +arr[i][cnt]+"\t");
}
//System.out.println("*********************************************");
}
if(arr.length % 2 == 1){
arr[(arr.length-1) / 2] [(arr.length-1) / 2] = h ;
}
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
System.out.print(arr[i][j] + "\t");
}
System.out.println();
}
}
}
4. 面向对象
4.0 base
4.0.1 面向对象 VS 面向过程
1. 面向对象 (OOP):
将功能封装进对象,强调具备了功能的对象,以 类 / 对象 位最小单位,考虑谁来做.
2. 面向过程 (POP):
强调的是功能行为,以函数为最小单位,考虑谁来做.
4.0.2 内存层面
编译完源程序后,生成一个或多个字节码文件。我们使用 JVM 中的类的加载器(可以想想反射时候用的 ClassLoader 获取类)和 解释器对生成的字节码文件进行解释运行,意味着,需要将字节码对应的类加载到内存中,涉及到内存解析。
ClassLoader classLoader = Demo.class.getClassLoader();
Class clazz = classLoader.loadClass("com.nuo.Demo");
1. 本地方法栈 :是java虚拟机在调用一些本地方法时需要给这些本地方法提供的内存空间,本地方法是指那些不是由java代码编写的方法,因为我们的java代码是有一定的限制的,它有的时候不能直接跟我们的操作系统底层打交道,所以就需要一些用c或c++编写的本地方法来与操作系统更底层的api来打交道,java代码可以间接地通过本地方法来调用到底层的一些功能,那这些本地方法运行的时候使用的内存就叫做本地方法栈。
2. 虚拟机栈 :平时提到的栈结构,局部变量存储在栈结构中。
3. 堆 :我们将 new 出来的结构(eg.数组、对象)加载在堆空间中,对象的属性(非 static 的)加载在堆空间中。
4. 方法区 :类的加载信息、静态域、常量池。
4.0.3 “万事万物皆对象”
(1)在Java语言范畴中,我们都将功能、结构等封装到类中, 通过类的实例化 , 来调用具体的功能结构
> Scanner,String等
> 文件:File
> 网络资源 :URL
(2)涉及到Java语言与前端Html、后端的数据库交互时,前后端的结构在Java层面交互时,都体现为类、对象
理解1:String,Date,Integer,Thread,NullPointerException,HttpServletRequest等
理解2:File : "E:\\Java"
URL : "http://127.0.0.1:8080/hello.txt"
理解3:类也是对象?(方法区)
-> Class 的实例对应一个运行时类
理解4:Java8中lambda表达式、方法引用
理解5:数据库中的表
理解6:html、xml等标签语言
面向对象思想落地实现的规则:
(1)创建类,设计类的成员
(2)创建类的对象 (类的实例化)
(3)通过对象调属性,调方法
4.0.4 Object 类
Object 类的使用
java.lang.Object类
1. Object类是所有Java类的根父类;
2. 如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类
3. Object类中的功能(属性、方法)就具有通用性。
4. 属性: 无
5. 方法:
1. 无参构造方法:
首先,他有一个 Object()无参构造方法。
2. toString
返回对象的字符串表示形式。默认返回的是类的名字+@符号+实例的哈希吗 16 进制的字符串。我们可以重写 toSting 方法返回想要的字符串。
3. equals
比较调用对象是否与参数对象相等。默认比较的是两个对象的地址值,在 object 中是相当于==的,但是一般会重写 equals 方法比较两个对象的属性值是否相等。
4. hashCode
返回和对象内存地址相关的一个整数值。
5. clone
创建并返回此对象的副本。
6. getClass
返回此 Object 当前的运行时的类。
7. notify
唤醒在此对象监视器上等待的单个线程。
8. notifyAll
唤醒在此对象监视器等待所有线程。
9. wait
使线程等待的方法,同时也会让当前线程释放它所持的锁。
10. finalize:
这个方法是释放内存用的,感觉很少会使用到。
6. Object 类只声明了一个空参的构造器。
4.0.5 == 和 equals 的区别
== 和 equals 的区别
一、回顾 == 的使用
== : 运算符
1.可以使用在基本数据类型变量和引用数据类型变量中
2.如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同)
如果比较的是引用数据类型变量:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体
补充: == 符号使用时,必须保证符号左右两边的变量类型一致。
二、equals () 方法的使用
1. 是一个方法,而非运算符
2. 只能适用于引用数据类型。
3. Object 类中 equals() 的定义:
说明 :Object 类中定义的 equals()和 == 的作用是相同的,比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体。
public class Object {
public boolean equals(Object var1) {
return this == var1;
}
}
4. 像String、Date、File、包装类等都重写了Object类中的equals()方法.不是比较两个引用的地址是否相同,而是比较两个对象的 "实体内容" 是否相同。
5. 通常情况下,我们自定义的类如果使用 equals() 的话,也通常是比较两个对象的 "实体内容" 是否相同。那么,我们就需要对 Object 类中的 equals() 进行重写。
重写的原则:比较两个对象的实体内容是否相同。
PS :
String & Date & File 等已重写过 equals 方法
PPS :
依旧特例 String :
如果直接 String str1 = "abc";
String str2 = "abc";
String str3 = "a";
=> 存在于常量池,str2 指向 str1 的地址 , 此时 str1 == str2 => true
=> str3 不等于 str1 ,虽然也存在于常量池, 但实际上新开辟了一处空间 (new),此时 str1 == str3 => false
如果 String str1 = new String("abc");
String str2 = new String("abc");
=> 存在于堆空间中 str1 == str2 => false
(可参考方法参数的值传递机制 )
4.0.6 toString()
toString() 方法的重写
1.当我们输出一个引用对象时,实际上就是调用当前对象的toString() 【存在特例】
2.Object类中toString的定义方法(输出地址)
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
3.像String、Date、File、包装类等都重写了Object类中的toString()方法。
使得在调用 toString()时,返回 "实体内容" 信息.
4.自定义类如果重写 toString() 方法,当调用此方法时,返回对象的 "实体内容".
4.0.7 单例的设计模式
单例的设计模式
1.所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例.并且该类只提供一个取得其对象实例的方法.如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为 private,这样,就不能用 new 操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象.因为在类的外部开始还无法得到类的对,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的.
2.如何实现?
饿汉式 VS 懒汉式
3.区分饿汉式和懒汉式.
饿汉式:坏处:对象加载时间过长。
好处:饿汉式是线程安全的。
懒汉式:好处:延迟对象的创建。
坏处:线程安全问题
//单例的饿汉式
class Bank{
//1.私有化类的构造器
private Bank(){
}
//2.内部创见类的对象
//4.要求此对象也必须声明为静态的
private static Bank instance = new Bank();
//3.提供公共的静态的方法,返回类的对象。
public static Bank getInstance(){
return instance;
}
}
//单例的懒汉式实现
class Order{
//1.私有化类的构造器
private Order(){
}
//2.声明当前类对象,没有初始化。
//此对象也必须声明为 static 的
private static Order instance = null;
//3.声明 public、static 的返回当前类对象的方法
public static Order getInstance(){
if(instance == null){
instance = new Order();
}
return instance;
}
}
单例模式的优点
由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
4.0.8 main 方法的理解
main()方法的理解
1.理解 main 方法的语法(了解)
2.由于 Java 虚拟机需要调用类的 main()方法,所以该方法的访问权限必须是 public,又因为 Java 虚拟机在执行 main()方法时不必创建对象,所以该方法必须是 static 的,该方法接收一个 String 类型的数组参数,该数组中保存执行 Java 命令时传递给所运行的类的参数。
3.又因为 main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。
4.main()方法的使用说明
1.main()方法作为程序的入口;
2.main()方法也是一个普通的静态方法
3.main()方法也可以作为我们与控制台交互的方式.(之前,使用 Scanner)
4.0.9 mvc
4.0.10 包装类的使用
JDK 5.0 新增 :自动装箱 & 自动拆箱
自动装箱 :基本数据类型 -> 包装类
eg. int -> Integer
自动拆箱 :包装类 -> 基本数据类型
eg. Integer -> int
坑 :
/**
* @description: TODO
* @author nuo
* @date 2022/7/12 23:58
* @version 1.0
*/
public class test {
public static void main(String[] args) {
Object o1 = true ? new Integer(1) : new Double(2.0);
System.out.println(o1); // 1.0
Object o2;
if (true) {
o2 = new Integer(1);
} else {
o2 = new Double(2.0);
}
System.out.println(o2); // 1
}
}
4.0.11"int" & “Integer”
1. int 是基本数据类型;Integer 是 java 为 int 提供的封装类。
2. int 的默认值为 0,而 Integer 的默认值为 null;
3. int 无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为 0 的区别,这时候就只能使用 Integer。
4. 另外,Integer 提供了多个与整数相关的操作方法,Integer 中还定义了表示整 数的最大值和最小值的常量。
/**
* @description: TODO
* @author nuo
* @date 2022/7/13 0:05
* @version 1.0
*/
public class test {
public static void main(String[] args) {
Integer integer1 = new Integer(1);
Integer integer2 = new Integer(1);
System.out.println(integer1 == integer2); // false
Integer integer3 = 1;
Integer integer4 = 1;
System.out.println(integer3 == integer4); // true
Integer integer5 = 128;
Integer integer6 = 128;
System.out.println(integer5 == integer6); // false
}
}
【PS】:Integer 内部定义了 IntegerCache 结构,IntegerCache 中定义了 Integer[] ,保存了从 -128 ~ 127 范围的整数,如果我们使用自动装箱的方式,给 Integer 赋值在 -128 ~ 127 范围内时,可以直接使用数组中的元素,不用再去 new 了,从而提高效率。
public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int var0) {
return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
private IntegerCache() {
}
static {
int var0 = 127;
String var1 = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
int var2;
if (var1 != null) {
try {
var2 = Integer.parseInt(var1);
var2 = Math.max(var2, 127);
var0 = Math.min(var2, 2147483518);
} catch (NumberFormatException var4) {
}
}
high = var0;
cache = new Integer[high - -128 + 1];
var2 = -128;
for(int var3 = 0; var3 < cache.length; ++var3) {
cache[var3] = new Integer(var2++);
}
assert high >= 127;
}
}
}
4.1 类和类的成员
4.1.1 属性 (成员变量) VS 局部变量
(非 static)成员变量和局部变量的区别:
(1)在类中的位置不同
成员变量:类中方法外(属性)
局部变量:方法定义中或者方法声明上,代码块内
(2)在内存中的位置不同
成员变量:在堆中
局部变量:在栈中
(3)生命周期不同
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:随着方法的调用而存在,随着方法的调用完毕而消失 (后进先出)
(4)初始化值不同
成员变量:有默认值
局部变量:没有默认值,必须先定义,赋值,然后才能使用
属性的赋值过程 :
1.默认初始化
2.显式初始化
3.构造器中初始化
4.通过 "对象.属性" or "对象.方法"的方式进行赋值
4.1.2 方法
(1)方法的重载
在同一个类中,允许存在一个以上的同名方法,只有它们的参数个数或参数类型不同即可
=>"两同一不同"
同一个类,相同的方法名
参数列表不同 : 参数个数 && 参数类型
判断是否构成重载
与方法的权限修饰符,返回值类型,形参变量名,方法体都没有关系.
在通过对象调方法时,如何确定某一个指定的方法
方法名 => 参数列表
(2)方法的重写
方法的重写(override / overwrite)
1.重写:子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作
2.应用:重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行 的是子类重写父类的方法。
3. 重写的规定:
方法的声明:
权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型{
//方法体
}
约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法
① 子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同
② 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符 >特殊情况:子类不能重写父类中声明为 private 权限的方法
③ 返回值类型:
>父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void
>父类被重写的方法的返回值类型是 A 类型,则子类重写的方法的返回值类型可以是 A 类或 A 类的 子类 >父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型 必须是相同的基本数据类型 (必须也是 double)
④ 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型
【PS】子类和父类中的同名同参数的方法要么都声明为非 static 的(考虑重写),要么都声明为 static 的(不是重写)。
(3)可变形参的方法
1. 格式 : 数据类型 ... 变量名
2. 可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载
3. 可变个数形参的方法与本类中方法名相同,形参类型也相同的数组之间不构成重载,且不可共存
4. 可变个数形参在方法中,必须声明在末尾
5. 可变个数形参在方法的形参中,最多只能声明一个
(4)方法的值传递机制
形参 => 方法声明时的参数
实参 => 方法调用时实际传给形参的参数值
Java 里方法的参数传递方式只有一种 : 值传递,即将实际参数值的副本传入方法内, 而参数本身不受影响
形参是基本数据类型 => 传"数据值"
形参是引用数据类型 => 传"地址值"
【PS】:String 为引用数据类型存于常量池中,but
public static void main(String[] args) {
String x = new String("ab");
change(x);
System.out.println(x); // "ab"
}
public static void change(String x) {
x = "cd";
}
why?
方法操作参数变量时是拷贝了变量的引用,而后通过引用找到变量(在这里是对象)的真正地址,并对其进行操作。当该方法结束后,方法内部的那个参数变量随之消失。但是要知道这个变量只是对象的一个引用而已,它只是指向了对象所在的真实地址,而非对象本身,所以它的消失并不会带来什么负面影响。回头来看原型变量,原型变量本质上也是那个对象的一个引用(和参数变量是一样一样的),当初对参数变量所指对象的改变就根本就是对原型变量所指对象的改变。所以原型变量所代表的对象就这样被改变了,而且这种改变被保存了下来。【本质单纯建了个副本】
故 相当于在常量池中重新 new 了一个String,使 s 指向新 new 的String, str 指向 main 方法中 new 的String
底层重载方法导致 :
why ? => println(char[])为特例,输出具体内容
-> public void println(char[] x)
Prints an array of characters and then terminate the line. This method behaves as though it invokes print(char[]) and then println()
=> println(int[])输出地址值[底层为 println(Object obj)]
-> public void println(Object x)
Prints an Object and then terminate the line. This method calls at first String.valueOf(x) to get the printed object's string value, then behaves as though it invokes print(String) and then println().
(5)递归
4.1.3 构造器
构造器 :
1.作用 :
创建对象
初始化对象的属性
2.特征:
具有与类相同的名称
不声明返回值类型 (与 void 不同)
不能被 static , final , synchronized , abstract , native 修饰,不能有 return 语句返回值
3.说明 :
1. 如果没有显示的定义类的构造器的话,则系统默认提供一个空参构造器
2. 定义构造器的格式:权限修饰符 类名 (形参列表){}
3. 一个类中定义的多个构造器,彼此构成重载
4. 一旦我们显式的定义了类的构造器后,系统就不再提供默认的空参构造器
5. 一个类中,至少会有一个构造器
4.1.4 代码块
代码块 :
1.代码块的作用:用来初始化类、对象的
2.代码块如果有修饰的话,只能使用 static
3.分类:静态代码块 vs 非静态代码块
4.静态代码块
> 内部可以有输出语句
> 随着类的加载而 执行 ,而且只执行一次
> 作用:初始化类的信息
> 如果一个类中,定义了多个静态代码块,则按照声明的先后顺序执行
> 静态代码块的执行,优先于非静态代码块的执行
> 静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构
5.非静态代码块
> 内部可以有输出语句
> 随着对象的创建而执行
> 每创建一个对象,就执行一次非静态代码块。
> 作用:可以在创建对象时,对对象的属性等进行初始化。
> 如果一个类中,定义了多个非静态代码块,则按照声明的先后顺序执行
> 非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法。
6.对属性可以赋值的位置:
① 默认初始化
② 显式初始化
③ 构造器中初始化
④ 有了对象以后,可以通过"对象.属性"或"对象.方法"的方式,进行赋值。
⑤ 在代码块中赋值
初始化顺序: ① -> ② / ⑤ -> ③ -> ④
7.静态初始化块:
由父类到子类,静态先行
class Root{
static{
System.out.println("Root 的静态初始化块");
}
{
System.out.println("Root 的普通初始化块");
}
public Root(){
System.out.println("Root 的无参数的构造器");
}
}
class Mid extends Root{
static{
System.out.println("Mid 的静态初始化块");
}
{
System.out.println("Mid 的普通初始化块");
}
public Mid(){
System.out.println("Mid 的无参数的构造器");
}
public Mid(String msg){
//通过 this 调用同一类中重载的构造器
this();
System.out.println("Mid 的带参数构造器,其参数值:"+ msg);
}
}
class Leaf extends Mid{
static{
System.out.println("Leaf 的静态初始化块");
}
{
System.out.println("Leaf 的普通初始化块");
}
public Leaf(){
//通过 super 调用父类中有一个字符串参数的构造器
super("nuo");
System.out.println("Leaf 的构造器");
}
}
public class LeafTest{
public static void main(String[] args){
new Leaf();
// Root 的静态初始化块
// Mid 的静态初始化块
// Leaf 的静态初始化块
// Root 的普通初始化块
// Root 的无参数的构造器
// Mid 的普通初始化块
// Mid 的无参数的构造器
// Mid 的带参数构造器,其参数值:nuo
// Leaf 的普通初始化块
// Leaf 的构造器
}
}
4.1.5 内部类
内部类 :
当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。
1. Java 中允许将一个类A声明在另一个类B中,则类A就是内部类,类B就是外部类.
2. 内部类的分类:
成员内部类 (直接在类内部声明类) VS 局部内部类 (方法内、代码块内、构造器内)
3. 成员内部类
> 作为外部类的成员
- 调用外部类的结构
- 可以被static修饰
- 可以被4种不同的权限修饰
> 作为一个类
- 类内可以定义属性、方法、构造器等
- 可以被final修饰,表示此类不能被继承。言外之意,不使用final,就可以被继承
- 可以abstract修饰
4.关注如下的3个问题
> 如何实例化成员内部类的对象
eg. class A{
static class B{
}
class C{
}
}
1.静态内部类的实例化
A.B b = new A.B();
方法的调用 => b.方法
2.非静态内部类的实例化
A a = new A();
a.C c = a.new C();
方法的调用 => c.方法
> 如何在成员内部类中区分调用外部类的结构
class A {
int a = 1;
int num = 1;
public void eat() {
System.out.println("A");
}
class B {
int b = 2;
int num = 2;
public void eat() {
System.out.println("B");
}
// num = 3
public void show(int num) {
System.out.println(num); // 3
System.out.println(A.this.num);// 1
System.out.println(this.num); // 2
System.out.println(a); // 1
System.out.println(b); // 2
A.this.eat(); // A
this.eat(); // B
}
}
}
> 开发中局部内部类的使用
public Comparable getComparable() {
return new Comparable() {
@Override
public int compareTo(Object o) {
return 0;
}
};
}
补充 :
/*
* 在局部内部类的方法中(比如:show)如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,要求此局部变量声明为final的。
* jdk 7及之前版本:要求此局部变量显式的声明为final的
* jdk 8及之后的版本:可以省略final的声明
*/
4.2 面向对象三大特征
4.2.1 封装性
1. 封装性 :
=> 隐藏对象内部的复杂性,只对外公开简单的接口.便于外界调用, 从而提高系统的可扩展性,可维护性.把该隐藏的隐藏起来, 该暴露的暴露出来, 这就是封装性的设计思想.
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外的提供的接口来访问该对象。
2. "高内聚" & "低耦合"
高内聚 :类的内部数据操作细节自己完成,不允许外部干涉
低耦合 :仅对外暴露少量的方法用于使用
3. Java 中规定的四种权限修饰符 【范围 : 小到大】
private => 缺省 => protected => public
4. JavaBean => 一种用 Java 语言写成的可重用组件 :
1.类是公共的
2.有一个无参的公共的构造器
3.有属性,且有对应的 get / set 方法
4.2.2 继承性
1. 继承性 :
1.继承性的好处 :
减少了代码的冗余 ,提高了代码的复用性
便于功能的拓展
为之后多态功能的使用提供了前提
2.格式 :
class A extends B{}
extends => 拓展
A :子类 ,派生类 ,subclass
B :父类 ,超类 ,基类 ,superclass
3.体现 :
一旦子类 A 继承 父类 B 后,子类 A 中就获取了父类 B 中所声明的所有属性和方法 【PS : 特别的 , 父类中声明为 private 的属性或方法 , 子类继承父类后 ,仍然认为获取了父类中私有的结构 , 只是因为封装性的影响 , 使得子类不能直接调用父类的结构而已】
子类继承父类以后 , 还可以声明自己特有的属性或方法 => 实现功能的拓展
子类和父类的关系不同于数学中子集和集合的关系.
> Java 只支持单继承和多层继承 , 不允许多重继承
4.Java 继承的相关规定
1.一个类可被多个子类继承
2.Java 中的单继承性 => 一个类只能有一个父类
3.子父类是相对的概念
4.直接父类 & 间接父类
5.子类继承父类以后就获取了直接父类以及所有间接父类中声明的属性和方法
2. 子类对象实例化过程 :
1.从结果上看:
子类继承父类以后,就获取了父类中声明的属性或方法.
创建子类的对象中,在堆空间中,就会加载所有父类中声明的属性.
2.从过程上看:
当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类构造器, 直到调用了 java.lang.Object 类中空参的构造器为止.正因为加载过所有的父类结构,所以才可以看到内存中有父类中的结构,子类对象可以考虑进行调用
明确:虽然创建子类对象时,调用了父类的构造器,但自始至终就创建过一个对象,即为 new 的子类对象。
4.2.3 多态性
1. 多态性 :
1.理解多态性:
1) 可以理解为一个事物的多种态性。
2) 实现代码的通用性
3) Object 类中定义的 public boolean equals(Object obj){}
JDBC : 使用Java 程序操作(获取数据库的连接 ,CRUD) 数据库(MySQL ,Oracle ,DB2 ,SQL Server)
4) 抽象类 , 接口的使用 (如果没有多态性 ,抽象类和 接口就无法实例化)
2.何为多态性:
对象的多态性:父类的引用指向子类的对象(或子类的对象赋值给父类的引用)
3.多态的使用:虚拟方法调用
有了对象多态性以后,我们在编译期,只能调用父类声明的方法,但在执行期实际执行的是子类重写父类的方法
简称:编译时,看左边;运行时,看右边。
若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
多态情况下,
“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
4.多态性的使用前提:
① 类的继承关系
② 方法的重写
5.对象的多态性:只适用于方法,不适用于属性(编译和运行都看左边)
2. 虚拟方法调用
从编译和运行的角度看:
重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。所以:
对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;
而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
PS : 多态是运行时行为 => 动态绑定
证明如下 :
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class InterviewTest {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}
public static void main(String[] args) {
int key = new Random().nextInt(3);
System.out.println(key);
Animal animal = getInstance(key);
animal.eat();
}
}
3. 通过向下转型调用子类所特有的属性和方法
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类的属性和方法不能调用。
如何才能调用子类所特有的属性和方法?
使用强制类型转换符,也可称为:向下转型
4. 向上转型 & 向下转型
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.eat();
Man man = new Man();
man.eat();
man.age = 25;
man.earnMoney();
// ************************************
System.out.println("************************");
// 对象的多态性,父类的引用指向子类的对象
Person p2 = new Man();
// Person p3 = new Woman();
// 多态的使用:当调用子父类同名同参数方法时,实际调用的是子类重写父类的方法---虚拟方法调用
p2.eat();
p2.walk();
// p2.earnMoney();
System.out.println("**************************");
// 不能调用子类所特有的方法、属性,编译时,p2是Person类型,
// p2.earnMoney();
p2.name = "Tom";
// p2.isSmoking = true;
// 有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类的属性和方法不能调用。
// 如何才能调用子类所特有的属性和方法?
// 使用强制类型转换符,也可称为:向下转型
Man m1 = (Man) p2;
m1.earnMoney();
m1.isSmoking = true;
// 使用强转时,可能出现 ClassCastException 异常
// Woman w1 = (Woman)p2;
// w1.goShopping();
/*
* instanceof关键字的使用
*
* a instanceof A:判断对象a是否是类A的实例。如果,返回true,如果不是,返回false;
*
* 使用情境:为了避免在向下转型时出现ClassCastException异常,我们在进行向下转型之前,先进行
* instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
*
* 如果a instanceof A返回true,则a instanceof B也返回true。 其中类B是类A的父类。
*
*/
if (p2 instanceof Woman) {
Woman w1 = (Woman) p2;
w1.goShopping();
System.out.println("**********Woman*********");
}
if (p2 instanceof Man) {
Man m2 = (Man) p2;
m2.earnMoney();
System.out.println("*********Man************");
}
if (p2 instanceof Person) {
System.out.println("***********Person************");
}
if (p2 instanceof Object) {
System.out.println("***********object************");
}
//向下转型的常见问题
//练习
//问题1:编译时通过,运行时不通过
//举例一
// Person p3 = new Woman();
// Man m3 = (Man)p3;
//举例二
Person p4 = new Person();
Man m4 = (Man)p4;
//问题二:编译通过,运行时也通过
Object obj = new Woman();
Person p = (Person)obj;
//问题三:编译不通过
// Man m5 = new woman();
// String str = new Date();
// Object o = new Date();
// String str1 = (String)o;
}
}
4.3 其他关键字
4.3.1 return
1. 使用范围 :使用在方法体中
2. 作用 :
2.1 结束方法
2.2 针对有返回值类型的方法,使用 'return 数据' 返回所要的数据
4.3.2 this
this 关键字 :
1. 可理解为 当前对象 or 当前正在创建的对象
2. this 可以用来调用属性、方法、构造器
3. this 的使用 :
3.1 我们可以在类的方法或构造器中通过 "this.属性" or "this.方法" 的方式显示的调用当前对象的属性或方法,通常省略。
3.2 特殊情况 :当方法的形参和类的属性同名时,用 "this.属性" 的方式表明此变量为属性而非形参
3.3 特殊情况 :当子类重写了父类的方法后,用 "this.方法" 调用子类重写父类的方法,可省略
4. this 调用构造器:
1.我们在类的构造器中可以使用 "this(形参列表)" 的方式 , 调用本类中其它的指定的构造器
2.构造器中 , 不可使用 "this(形参列表)" 的方式调自己
3.如果有 n 个构造器 , 则至少有 n - 1 个构造器中使用了 "this(形参列表)"
4.规定 : "this(形参列表)" 必须声明在当前构造器的首行
5.构造器内部,最多只能声明一个 "this(形参列表)" ,用来调用其他的构造器
4.3.3 super
super 关键字的使用 :
1. super 理解为 :父类的
2. super 可以用来调用属性、方法、构造器
3. super 的使用 :
3.1 我们可以在子类的方法或构造器中通过 "super.属性" or "super.方法" 的方式显示的调用父类中声明的属性或方法,通常省略。
3.2 特殊情况 :当子类和父类定义了同名的属性时,须显示的用 "super.属性" 的方式进行调用。
3.3 特殊情况 :当子类重写了父类的方法后,须用 "super.方法" 调用父类中被重写的方法。
4. super 调用构造器 :
4.1 可以在子类的构造器中显示的使用 "super(形参列表)" 的方式,调用父类中声明的指定的构造器
4.2 "super(形参列表)" 必须声明在子类构造器的首行
4.3 "this(形参列表)" 和 "super(形参列表)" 只能二选一,不能同时出现
4.4 没显式声明的话默认调用"super(形参列表)"
4.5 在类的多个构造器中至少有一个调用 "super(...)",否则 "this(...)" 循环调用报错
4.3.4 import
import 关键字的使用:
import:导入
1.在源文件中显式的使用import结构导入指定包下的类、接口
2. 声明在包的声明和类的声明之间
3.如果需要导入多个结构,则并列写出即可
4. 可以使用"xxx. *"的方式,表示可以导入XXx包下的所有结构
5.如果使用的类或接口是java. lang包下定义的,则可以省略import结构
6.如果使用的类或接口是本包下定义的,则可以省略import结构
7. 如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示。
8.使用"xXX. *"方式表明可以调用xxx包下的所有结构。但是如果使用的是xxx子包下的结构,则仍需要显式的使用import结构导入
9. import static:导入指定类或接口中的静态结构:属性或方法。
4.3.5 package
"package" 关键字的使用 :
1. 为了更好的实现项目中类的管理,提供包的概念
2. 使用 "package" 声明的类或接口所属的包,声明在源文件的首行
3. 包,属于标识符,遵循标识符的命名规范
4. 每 '.' 一次,就代表一层文件目录
【PS】:同一个包下,不能命名同名的接口、类
不同的包下,可以命名同名的接口、类
4.3.6 static
static 关键字的使用
1.static :静态的.
2.static 可以用来修饰:属性、方法、代码块、内部类.
3.使用 static 修饰属性:静态变量(或类变量).
3.1 属性:是否使用 static 修饰,又分为:静态属性 VS 非静态属性 (实例变量)
实例变量:我们创建了类的多个对象,每个对象都独立的拥有了一套类中的非静态属性,当修改其中一个非静态属性时,不会导致其他对象中同样的属性值的修饰.
静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量。当通过静态变量去修改某一个变量时,会导致其他对象调用此静态变量时,是修改过的.
3.2 static 修饰属性的其他说明:
① 静态变量随着类的加载而加载.可以通过"类.静态变量"的方式进行调用.
② 静态变量的加载要早于对象的创建.
③ 由于类只会加载一次,则静态变量在内存中也只会存在一次。存在方法区的静态域中.
④ 类变量 实例变量
类 yes no
对象 yes yes
3.3 静态属性举例:System.out.Math.PI;
4.使用 static 修饰方法:静态方法
① 随着类的加载而加载,可以通过"类.静态方法"的方式调用
② 静态方法 非静态方法
类 yes no
对象 yes yes
③ 静态方法中,只能调用静态的方法或属性
非静态的方法中,可以调用所有的方法或属性
5.static 注意点:
5.1 在静态的方法内,不能使用 this 关键字、super 关键字
5.2 关于静态属性和静态方法的使用,大家从生命周期的角度去理解。
6.开发中,如何确定一个属性是否需要声明 static 的?
> 属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
> 类中的常量也常常声明为 static
开发中,如何确定一个方法是否要声明为 static 的?
> 操作静态属性的方法,通常设置为 static 的
> 工具类中的方法,习惯上声明为 static 的。比如:Math、Arrays、Collections
4.3.7 final
关键字:final
final :最终的
1. final 可以用来修饰的结构 :类、方法、变量
2. final 用来修饰一个类:此类不能被其他类所继承。
比如 :String 类、System 类、StringBuffer 类
3. final 修饰一个方法: final 标记的方法不能被子类重写。
比如 :Object 类中的 getClass()。
4. final 用来修饰变量:此时的"变量"(成员变量或局部变量)就是一个常量。名称大写,且只能被赋值一次。
4.1 final 修饰属性,可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化
4.2 final 修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。
static final 用来修饰:全局常量
4.3.8 abstract
抽象类与抽象方法
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
abstract 关键字的使用
1.abstract:抽象的
2.abstract 可以用来修饰的结构:类、方法
3.abstract 修饰类:抽象类
> 此类不能实例化
> 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化全过程)
> 开发中,都会提供抽象类的子类,让子类对象实例化,实现相关的操作
4.abstract 修饰方法:抽象方法
> 抽象方法,只有方法的声明,没有方法体。
> 包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法
> 若子类重写了父类中所有的抽象方法,此子类,
abstract 使用上的注意点:
1.abstract 不能用来修饰变量、代码块、构造器;
2.abstract 不能用来修饰私有方法、静态方法、final 的方法、final 的类。
多态的应用:模板方法设计模式(TemplateMethod)
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
解决的问题:
当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。
/*
* 抽象类的应用:模板方法的设计模式
*/
public class TemplateTest {
public static void main(String[] args) {
SubTemlate t = new SubTemlate();
t.sendTime();
}
}
abstract class Template{
//计算某段代码执行所需花费的时间
public void sendTime(){
long start = System.currentTimeMillis();
code(); //不确定部分,易变的部分
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
public abstract void code();
}
class SubTemlate extends Template{
@Override
public void code() {
for(int i = 2;i <= 1000;i++){
boolean isFlag = true;
for(int j = 2;j <= Math.sqrt(i);j++){
if(i % j == 0){
isFlag = false;
break;
}
}
if(isFlag){
System.out.println(i);
}
}
}
}
4.3.9 interface
接口(interface)
概述:
一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java 不支持多重继承。有了接口,就可以得到多重继承的效果。
另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有 is-a 的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3 机、手机、数码相机、移动硬盘等都支持 USB 连接。
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个"是不是"的关系,而接口实现则是"能不能"的关系。
接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守。
接口(interface) 是抽象方法和常量值定义的集合。
接口的特点:
1.用 interface 来定义。
2.接口中的所有成员变量都默认是由 public static final 修饰的。
3.接口中的所有抽象方法都默认是由 public abstract 修饰的。
4.接口中没有构造器。
5.接口采用多继承机制。
接口的使用
1.接口使用 interface 来定义。
2.在 Java 中:接口和类是并列的两个结构
3.如何去定义两个接口:定义接口中的成员
3.1 JDK7 :及以前:只能定义全局常量和抽象方法
>全局常量:public static final 的,但是书写中,可以省略不写。
>抽象方法:public abstract 的
3.2 JDK8 :除了全局常量和抽象方法之外,还可以定义静态方法、默认方法(略)。
4.接口中不能定义构造器!意味着接口不可以实例化。
5.Java 开发中,接口通过让类去实现 (implements) 的方式来使用。
如果实现类覆盖了接口中的所有方法,则此实现类就可以实例化
如果实现类没有覆盖接口中所有的抽象方法,则此实现类仍为一个抽象类
6.Java 类可以实现多个接口 ---》弥补了 Java 单继承性的局限性
格式:class AA extends BB implementd CC,DD,EE
7.接口与接口之间是继承,而且可以多继承
8.接口的具体使用,体现多态性
接口的主要用途就是被实现类实现。(面向接口编程)
9.接口,实际可以看作是一种规范
面试题:抽象类与接口有哪些异同?
抽象类的特性!
不可以被实例化
含有声明但未实现的方法(也可以包含已实现的方法)
一个类只能继承一个抽象类
一旦有了抽象方法,就一定要把这个类声明为抽象类
子类必须覆盖抽象类的抽象方法
什么是接口?
接口,它是对行为的抽象,而具体如何行动需要由子类去实现,接口的意义在于抽象,不拘细节,从而使同类事物在在同一高度具有通用性及可替代性。
接口的特性!
不可以被实例化
含有声明但未实现的方法
一个类可以继承多个接口
子类必须实现其声明未实现的方法 或者 该类为抽象类
所有成员都是默认Public的,因此接口中不能有Private成员
子类必须实现接口的所有成员
从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
接口与抽象类的区别:
相同点:
都包含抽象方法,其子类都必须重写这些抽象方法
都不能直接实例化对象
都位于继承的顶端,用于被其他类实现或者继承
区别 :
抽象类里面可以包含普通成员方法,接口不能包含普通成员方法
一个类只能直接继承一个父类(可以是抽象类),一个类也可以实现多个接口
类与类之间只能时单继承关系,接口与接口之间可以多继承
抽象类可以定义普通的成员变量和常量,接口只能定义常量 public static final 修饰的
/*
* 接口的使用
* 1.接口使用上也满足多态性
* 2.接口,实际上就是定义了一种规范
* 3.开发中,体会面向接口编程!
*/
接口的应用:代理模式(Proxy)
代理模式是 Java 开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。
/*
* 接口的应用:代理模式
*/
public class NetWorkTest {
public static void main(String[] args) {
Server server = new Server();
// server.browse();
ProxyServer proxyServer = new ProxyServer(server);
proxyServer.browse();
}
}
interface NetWork{
public void browse();
}
//被代理类
class Server implements NetWork{
@Override
public void browse() {
System.out.println("真实的服务器来访问网络");
}
}
//代理类
class ProxyServer implements NetWork{
private NetWork work;
public ProxyServer(NetWork work){
this.work = work;
}
public void check(){
System.out.println("联网前的检查工作");
}
@Override
public void browse() {
check();
work.browse();
}
}
应用场景:
安全代理:屏蔽对真实角色的直接访问。
远程代理:通过代理类处理远程方法调用(RMI)
延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象
比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有 100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用 proxy 来进行大图片的打开。
分类
静态代理(静态定义代理类)
动态代理(动态生成代理类)
JDK 自带的动态代理,需要反射等知识
接口的应用:工厂模式
< 略 >
Java 8 中关于接口的改进
Java 8 中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个抽象定义的理念。
静态方法:
使用 static 关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中找到像 Collection/Collections 或者 Path/Paths 这样成对的接口和类。
默认方法:
默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如:java 8 API 中对 Collection、List、Comparator 等接口提供了丰富的默认方法。
接口相关的方法调用:
1.接口中定义的静态方法,只能通过接口来调用
2.通过实现类的对象,可以调用接口中的默认方法
3.如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的 默认 方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。-->类优先原则 <相当于对默认方法进行了重写>
4.如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错。-->接口冲突。这就需要我们必须在实现类中重写此方法
5.如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
调用自己定义的重写的方法 => 方法
调用的是父类中声明的方法 => super.方法
调用接口中的默认方法 => 接口.super.方法
5. 异常处理
5.1 异常概述 & 异常体系结构
1. 异常 :在 Java 语言中,将程序执行中发生的不正常情况称为 "异常"。(开发过程中的语法错误和逻辑错误不是异常)
2. 分类 :
2.1 Error :Java 虚拟机无发解决的严重问题。比如:JVM 系统内部错误、资源耗尽等严重错误。
eg. StackOverflowError 和 OOM,一般不编写针对性的代码进行处理
2.2 Exception :其它编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。eg.
空指针访问
试图读取不存在文件
网络连接终中断
数组角标越界
3. 运行时异常
3.1 是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子
类都是运行时异常。
3.2 对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
4. 编译时异常
4.1 是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。
4.2 对于这类异常,如果程序不处理,可能会带来意想不到的结果。
5.2 常见异常
5.3 异常处理机制一 :try - catch - finally
try {
// 可能出现异常的代码
} catch (Exception1 e) {
// 异常处理 1
} catch (Exception2 e) {
// 异常处理 2
} catch (Exception3 e) {
// 异常处理 3
} finally {
// 就算catch 中异常,finally 也一定会被执行 ,之后才执行 try 或者 catch 中的 return
// 常用于资源的关闭
}
5.4 异常处理机制二 :throws
权限修饰符 返回值类型 method throws 异常类型 (args) {
}
【PS】:子类重写方法的异常不大于父类被重写的方法抛出的异常
5.5 用户手动抛异常 :throw
throw new 异常 (); // 产生异常,并非处理异常
5.6 用户自定义异常类
class MyException extends RuntimeException {
static final long serialVersionUID = -212131342343424234L;
public MyException() {}
public MyException(String msg) {
super(msg);
}
}
6. 多线程
6.1 基本概念 :程序、进程、线程
1. 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
2. 进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
2.1 如:运行中的QQ,运行中的MP3播放器
2.2 程序是静态的,进程是动态的
2.3 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
3. 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
3.1 若一个进程同一时间并行执行多个线程,就是支持多线程的
3.2 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
3.3 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
1. 单核CPU和多核CPU的理解
1.1 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时
间单元特别短,因此感觉不出来。
1.2 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
1.3 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
2. 并行与并发
2.1 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
2.2 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
6.2 线程的创建和使用 (2)
6.2.1 多线程
1. 多线程程序的优点:
1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2. 提高计算机系统CPU的利用率
3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
2. Java语言的JVM允许程序运行多个线程,它通过 java.lang.Thread 类来体现。
3. Thread类的特性
3.1 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把 run() 方法的主体称为线程体
3.2 通过该 Thread 对象的 start() 方法来启动这个线程,而非直接调用 run()
6.2.2 Thread 类
1. 构造器
1.1 Thread():创建新的Thread对象
1.2 Thread(String threadname):创建线程并指定线程实例名
1.3 Thread(Runnable target):指定创建线程的目标对象,它实现了 Runnable 接口中的run方法
1.4 Thread(Runnable target, String name):创建新的Thread对象
2. 方法
2.1 void start(): 启动线程,并执行对象的run()方法
2.2 run(): 线程在被调度时执行的操作
2.3 String getName(): 返回线程的名称
2.4 void setName(String name):设置该线程名称
2.5 static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
2.6 static void yield():线程让步
-> 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
-> 若队列中没有同优先级的线程,忽略此方法
2.7 join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
-> 低优先级的线程也可以获得执行
2.8 static void sleep(long millis):(指定时间:毫秒)
-> 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
-> 抛出InterruptedException异常
2.9 stop(): 强制线程生命期结束,不推荐使用
2.10 boolean isAlive():返回boolean,判断线程是否还活着
6.2.3 创建线程的方式一 :继承 Thread 类
步骤:
1.创建一个类,使它继承于 Thread 类
2.重写 Thread 类中的 run 方法
3.实例化子类
4.调用 Thread 类 中声明的 start() 方法
【PS】:如果出现子类的多个实例化对象去操作一个数 (eg.抢票)一般将这个数设置为 static ,但仍会有线程安全的问题,可类比面向对象时学的懒汉式单例模式存在的线程安全
public class testCreatThread1 {
public static void main(String[] args) {
// start() => 1.启动当前线程
// 2.调用当前线程的 run()
new CreatThread1().start();
}
}
class CreatThread1 extends Thread{
public CreatThread1() {
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0){
System.out.println(i);
}
}
}
}
6.2.4 创建线程的方式二 :实现 Runnable 接口
步骤 :
1.创建一个实现了Runnable接口得类
2.将线程要执行的逻辑声明在run()中
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
方法一和方法二的相同点:
1.两种方式都需要重写run()
2.实现类去实现Runnable中的抽象方法:run()
开发中:
优先选择:实现Runnable接口的方式
why ?
1.实现的方式更适合来处理多个线程有共享数据的情况
2.实现的方式没有类的单继承性的局限, but 仍会有类似方法一中的线程安全的问题
public class testCreatThread2 {
public static void main(String[] args) {
//3.创建实现类的对象
CreatThread2 c = new CreatThread2();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t = new Thread(c);
//5.通过Thread类的对象调用start()
// start() => 启动线程
// => 调用当前线程的 run
// public class Thread implements Runnable {
// private Runnable target;
// public void run() {
// if (this.target != null) {
// this.target.run();
// }
// }
// }
t.start();
new Thread(new CreatThread2()).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 3 == 0) {
System.out.println(getClass() + " : " + i);
}
}
}
}).start();
}
}
//1.创建一个实现了Runnable接口得类
class CreatThread2 implements Runnable {
//2.将线程要执行的逻辑声明在run()中
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(getClass() + " : " + i);
}
}
}
}
6.2.5 线程的优先级
线程的优先级等级 :
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5
涉及的方法 :
getPriority() :返回线程优先值
setPriority(int newPriority) :改变线程的优先级
说明 :
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
6.2.6 线程的分类
6.3 线程的生命周期
JDK中用 Thread.State 类定义了线程的几种状态 :
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在a它的一个完整的生命周期中通常要经历如下的五种状态:
1. 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
5. 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
6.4 线程的同步
6.4.1 同步代码块
// 同步监视器 => 锁,任何一个类的对象都可以充当锁
synchronized(同步监视器) {
// 操作共享数据,需要被同步的代码
}
// 多个线程必须共用同一把锁
【PS】: 实现 Runnable 接口时, 可以考虑使用 this 充当锁
好处 :解决了线程安全问题
局限性 :操作同步代码时,只能有一个线程参与,其它线程等待,相当于是一个单线程的过程,效率低
6.4.2 同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
【PS】:
1. 同步方法仍涉及到同步监视器,只是不需要显式声明
2. 非静态的同步方法,同步监视器 :this
静态的同步方法,同步监视器 :当前类本身
6.4.3 Demo
public class testSynchronized2 {
public static void main(String[] args) {
// => 俩对象 ,but 前提 : 同一把锁
Synchronized2_1 s1 = new Synchronized2_1();
Synchronized2_1 s2 = new Synchronized2_1();
s1.start();
s2.start();
}
}
class Synchronized2_1 extends Thread{
//方式一:同步代码块
@Override
public void run() {
//获取当前类作为对象,不可直接使用 this; 详情参上
//synchronized (this){
synchronized (Synchronized2_1.class){
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread());
}
}
}
}
class Synchronized2_2 extends Thread{
//方式二:同步方法 (可参照方式一)
@Override
public void run() {
show();
}
// 同步监视器未显式声明,默认为 this
// 继承 Thread 类的时候必须使用 static ,不然无法保证锁是同一个
private static synchronized void show(){
System.out.println(Thread.currentThread());
}
}
6.4.5 锁
Lock(锁)
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同
步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
/*
* 解决线程安全问题的方式三:lock锁--->JDK5.0新增
*
* 注意:如果同步代码有异常,要将unlock()写入finally语句块
*
* 1. 面试题:synchronized 与 Lock的异同?
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
*
* 2.优先使用顺序:
* Lock -> 同步代码块(已经进入了方法体,分配了相应资源)-> 同步方法(在方法体之外)
*
* 面试题:如何解决线程安全问题?有几种方式
*/
public class testLock {
public static void main(String[] args) {
Lock lock = new Lock();
new Thread(lock).start();
new Thread(lock).start();
new Thread(lock).start();
}
}
class Lock implements Runnable {
private int cnt = 0;
//1.实例化ReentrantLock
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法:lock()
lock.lock();
Thread.sleep(100);
if (cnt < 100) {
System.out.println(Thread.currentThread() + ":卖出第 " + (++cnt));
}else{
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
6.4.6 死锁
1. 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
2. 解决方法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
/*
* 演示线程的死锁
*
* 1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,
* 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
* 2.说明:
* > 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
* > 我们使用同步时,要避免出现死锁。
*/
public class deathLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
6.5 线程通信
1.wait() 与 notify() 和 notifyAll()
1.wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
2.notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
3.notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
2.这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException异常。
3.因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
6.6 sleep() 和 wait() 的异同
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
6.7 exer
/*
* 线程通信的应用:经典例题:生产者/消费者问题
*
* 生产者(com.nuo.Exer.Producer)将产品交给店员(com.nuo.Exer.Clerk),而消费者(com.nuo.Exer.Customer)从店员处取走产品,
* 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,
* 店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;
* 如果店中没有产品了,店员会告诉消费者等一下,
* 如果店中有产品了再通知消费者来取走产品。
*
* 分析:
* 1.是否是多线程的问题?是,生产者的线程,消费者的线程
* 2.是否有共享数据的问题?是,店员、产品、产品数
* 3.如何解决线程的安全问题?同步机制,有三种方法
* 4.是否涉及线程的通信?是
*/
public class ThreadExer2 {
public static void main(String[] args) {
Clerk c = new Clerk();
Producer p = new Producer(c);
Customer c_ = new Customer(c);
new Thread(p).start();
new Thread(c_).start();
}
}
class Producer implements Runnable {
private final Clerk c;
public Producer(Clerk c) {
this.c = c;
}
@Override
public void run() {
while (true) {
synchronized (c) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (c.getProduct() < 20) {
c.setProduct(c.getProduct() + 1);
System.out.println("生产者生产第" + c.getProduct() + "件产品...");
// 必须使用同步监视器去调用
c.notify();
}else {
try {
// 必须使用同步监视器去调用
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Customer implements Runnable {
private final Clerk c;
public Customer(Clerk c) {
this.c = c;
}
@Override
public void run() {
while (true) {
synchronized (c) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (c.getProduct() > 0) {
System.out.println("消费者消费第" + c.getProduct() + "件产品...");
c.setProduct(c.getProduct() - 1);
// 必须使用同步监视器去调用
c.notify();
}else {
try {
// 必须使用同步监视器去调用
c.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class Clerk {
private int product;
public int getProduct() {
return product;
}
public void setProduct(int product) {
this.product = product;
}
}
6.8 JDK 5.0 新增线程的创建方式 (2)
6.8.1 创建线程的方式三 :实现 Callable 接口
与使用 Runnable 相比,Callable 功能更强大些
1.相比 run()方法,可以有返回值
2.方法可以抛出异常
3.支持泛型的返回值
4.需要借助 FutureTask 类,比如获取返回结果
Future 接口
1.可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
2.FutrueTask是Futrue接口的唯一的实现类
3.FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
public class testCreatThread3 {
public static void main(String[] args) {
// 3.创建 Callable 接口实现类的对象
NumThread numThread = new NumThread();
// 4.将此 Callable 接口实现类的对象作为参数传递到 FutureTask 构造器中, 创建 FutureTask 对象
FutureTask futureTask = new FutureTask(numThread);
// 5.将 FutureTask 的对象作为参数传递到 Thread 的构造器中启动线程, 创建 Thread 对象, 调用 start() 启动线程
new Thread(futureTask).start();
try {
// 6.获取 Callable 中 call() 的返回值
// get() 返回值即为 FutureTask 构造器参数 Callable 接口实现类的返回值
// 如果不需要接收返回值, 则可不调用 get()
Object sum = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
// 创建一个 Callable 的实现类
class NumThread implements Callable {
// 实现 call()
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
6.8.2 创建线程的方式四 :使用线程池
背景:
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
3.1 corePoolSize:核心池的大小
3.2 maximumPoolSize:最大线程数
3.3 keepAliveTime:线程没有任务时最多保持多长时间后会终止
public class testCreatThread4 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new NumThread1()); // 适用于 Runnable
executorService.submit(new NumThread2()); // 适用于 Callable
// 关闭连接池
executorService.shutdown();
}
}
class NumThread1 implements Runnable {
@Override
public void run() {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
}
}
class NumThread2 implements Callable {
// 实现 call()
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}