本文适合有编程基础或是需要Java语言复习的家人们食用~
一、Java语言介绍
- 本篇文章使用的JDK版本是1.8(即JDK 8)
- Java语言是运行在JVM上的,有了JVM,Java语言得以在不同操作系统上运行
- 垃圾回收机制:Java语言提供了一种系统级线程追踪存储空间的分配情况,当JVM空闲时,检查并释放无用的存储空间,但仍然存在内存泄漏和内存溢出。而C/C++中需要由开发人员负责回收
- 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
- 内存溢出你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
二、编程基础语法
基本数据类型
整型
//byte: -128~127
//short: -2^15 ~ 2^15-1
//int: -2^31 ~ 2^31-1
//long: -2^63 ~ 2^63-1 定义需要后加'l',若不加则为int类型。如果数据不超过int范围不会报错
//整形常量默认类型为int型
byte b1 = 12;
short s1 = 200;
int i1 = 3400;
long l1 = 56009l;
浮点型
//float: 精确到7位有效数字 4字节
//double: 精确到14位有效数字 8字节
//浮点型常量默认为double类型,声明fload变量须后加'f'或'F'
double d1 = 12.45;
float f1 = 123.45f;
字符型
//char: 2字节
char c1 = 'r';
System.out.println("write a char var: " + c1);
布尔型
//boolean: true或false
boolean t1 = false;
自动类型提升(小范围类型和大范围类型运算,自动提升为大范围类型)
//char, byte, short类型之间做运算结果都是int型
byte b1 = 2;
int i1 = 200;
System.out.println(b1 + i1); //int类型,若用byte接收结果会报错
char c2 = 'a';
short s2 = 2;
System.out.println(c2 + s2); //int类型,char的范围小
强制类型转换(从大范围类型强制转换为小范围类型)
//1.语法:(transferType)
//2.可能导致精度损失
//3.其实char类型的本质也是小范围的数字类型,只不过是根据字符集编码以字符形式显示出来
int i3 = 99;
short s3 = (short) i3;
System.out.println("s3 = " + s3);
char c3 = (char) s3;
System.out.println("c3 = "+ c3);
String字符串
String s1 = "晚风轻轻柔柔 ~";
String s2 = "D";
System.out.println(s2 + s1);
//String和基本数据类型运算只能进行连接运算(+),结果都是String类型
String ageStr = "年龄:";
int age = 21;
String ageInfo = ageStr + age;
System.out.println(ageInfo);
String和基本数据类型转换
// String --> int/double
String ageStr = "23";
int age1 = Integer.parseInt(ageStr);
double age2 = Double.parseDouble(ageStr);
// int/double --> String
int i1 = 97;
double d1 = 34.5566;
String si1 = String.valueOf(i1);
String sd1 = Double.toString(d1);
String.format()
二进制、八进制、十六进制
- 二进制数值以0b或0B开头
- 八进制数值以0开头
- 十六进制数值以0x或0X开头
int n1 = 0b1100;
int n2 = 045;
int n3 = 0x2A;
System.out.println("n1 = " + n1);
System.out.println("n2 = " + n2);
System.out.println("n3 = " + n3);
运算符
算术运算符: + - * / % ++ –
赋值运算符:= += -= *= /= %=
比较运算符:> < >= <= != ==
逻辑运算符:& && | || ! ^
- &&若第一个条件不满足,后面条件就不再判断。而&要对所有的条件都进行判断(二者结果相同,但可能速度有些差别)
- || 若第一个条件满足,后面条件就不再判断。而 | 要对所有的条件都进行判断
位运算符:>>右移、<<左移、>>>无符号右移、&与运算、| 或运算、^异或、~取反
三元运算符:(条件表达式) ? 表达式1 : 表达式2
- 若条件表达式为true执行表达式1,为false执行表达式2
if
if(condition 1){
...
}else if(condition 2){
...
}else{
...
}
switch-case
根据表达式的值匹配选择。
-
如果在匹配的case中不加break,在匹配后会继续执行下一个case,直到break为止
-
case之后只能跟常量
switch(表达式){
case var1:
...
break;
case var2:
...
break;
default:
...
break;
}
for
for(初始变量; 循环条件; 迭代条件){
...
}
while / do-while
while(循环条件){
...
}
do{
...
}while(循环条件);
do-while至少执行一次循环体,但while可以不执行循环体
break / continue
break; //默认跳出最近的一层循环
continue; //默认跳过最近的此次循环
label1:for(...){
for(...){
...
break label1; //跳出指定的一层循环
}
}
数组
创建数组对象会在内存中开辟一整块连续的空间,而数组名代表这块连续空间的首地址。由此可知,数组的长度一旦确定就不可改变。
//数组静态初始化,直接赋值
int[] ids = new int[]{101, 102, 103};
//数组动态初始化,定义长度
String[] names = new String[5]; //默认值为null
int[] ages = new int[5]; //默认值为0
boolean[] bool = new boolean[5]; //默认值为false
/*数组默认的初始化值
int: 0
boolean: false
String等引用数据类型: null
*/
//调用指定元素
System.out.println(names);
System.out.println(names[1]);
System.out.println(ids[0]);
System.out.println(ages[2]);
System.out.println(bool[2]);
//获取数组长度
System.out.println(ids.length);
//遍历数组
for(int i=0; i<ids.length; i++){
System.out.println(ids[i]);
}
引用计数算法:判断内存中的某一片空间是否被栈中的变量指向,若没有指向,则进行回收释放(垃圾回收)
二维数组的使用
//二维数组静态初始化
int[][] arr1 = new int[][]{{1, 2}, {3, 4}, {5, 6}};
int[][] arr4 = {{1, 2}, {3, 4}, {5, 6}};
//二维数组动态初始化
String[][] arr2 = new String[3][2]; //看着比较明显
int[] arr3[] = new int[4][5];
boolean[][] arr5 = new boolean[2][2];
//获取二维数组指定位置的元素
System.out.println(arr1[1][1]);
//获取二维数组长度
System.out.println(arr1.length);
//遍历二维数组
for(int i=0; i<arr1.length; i++){
for(int j=0; j<arr1[i].length; j++){
System.out.print(arr1[i][j] + " ");
}
}
System.out.println();
//二维数组元素默认初始化值
System.out.println(arr2[0]); // 外层元素都是地址值,因为外层元素要指内层数组
System.out.println(arr2[0][0]); // null
System.out.println(arr3[0]);
System.out.println(arr3[0][0]); // 0
System.out.println(arr5[0]);
System.out.println(arr5[0][0]); // false
int[][] arr6 = new int[4][];
System.out.println(arr6[0]);//内层空间未开辟,由于外层元素类型是int[],引用数据类型,所以初始化值为null
案例:快速排序算法的实现
public static void Partition(int[] arr, int left, int right){
//递归终止条件
if(left >= right) return;
int pivot = arr[left];
int low = left;
int high = right;
while(low < high){
while(arr[high] >= pivot && low < high) high--;
//此时high指向的元素小于pivot
arr[low] = arr[high];
while(arr[low] < pivot && low < high) low++;
//此时low指向的元素大于等于pivot
arr[high] = arr[low];
}
//此时low==high,放入枢轴元素
arr[low] = pivot;
//递归左右分区
Partition(arr, left, low-1);
Partition(arr, low+1, right);
}
//快速排序算法实现
public static void QuickSortRun(){
int[] arr = new int[100];
for(int i=0; i<100; i++){
arr[i] = (int) (Math.random()*1000);
}
//排序前输出
System.out.print("before quicksort: ");
for(int i=0; i<arr.length; i++){
if(i > 0) System.out.print(" ");
System.out.print(arr[i]);
}
System.out.println();
//开始划分递归
Partition(arr, 0, arr.length-1);
//排序后输出
System.out.print("after quicksort: ");
for(int i=0; i<arr.length; i++){
if(i > 0) System.out.print(" ");
System.out.print(arr[i]);
}
System.out.println();
}
Arrays工具类
int[] arr1 = new int[]{34, 45, 10, 20};
int[] arr2 = new int[]{34, 10, 45};
int[] arr3 = new int[]{34, 45, 10, 20};
//判断数组是否相同
System.out.println(Arrays.equals(arr1, arr2));
System.out.println(Arrays.equals(arr1, arr3));
//输出数组
System.out.println(Arrays.toString(arr2));
//填充数组
Arrays.fill(arr2, 99);
System.out.println(Arrays.toString(arr2));
//排序
Arrays.sort(arr3);
System.out.println(Arrays.toString(arr3));
//二分查找(适用于有序数组),找不到返回负数
System.out.println(Arrays.binarySearch(arr3, 34));
System.out.println(Arrays.binarySearch(arr3, 88));
//复制数组
int[] arr4 = Arrays.copyOf(arr3, arr3.length);
System.out.println(Arrays.toString(arr4));
三、面向对象
- Java类及类成员:属性、方法、构造器、代码块、内部类
- 面向对象的三大特征:封装、继承、多态
- 面向过程(POP)强调功能行为,以函数为最小单位,考虑怎么做,是一个动作或行为。面向对象(OOP)强调具备了功能的对象,以类/对象为最小单位,考虑谁来做,是一个事物。
构造器
构造器用于创建对象。
- 如果没有显式定义类的构造器的话,系统默认提供一个空参的构造器
- 可以自定义带参数的构造器,通常用于创建对象时自动初始化属性
修饰符
访问修饰符
- public:所有类可见
- protected:同一包内的类和所有子类可见(不能修饰外部类)
- private:同一类可见(不能修饰外部类)
- default(不加修饰符):对同一个包内的类可见
public class Person{
//同一类可见(不能修饰外部类)
private String name;
private int age;
private boolean isMale;
//同一包内的类和所有子类可见(不能修饰外部类)
protected String studentId;
//所有类可见
public String appearance;
//同一类可见
private void cry(){
System.out.println("cry > <...");
}
//同一包内的类和所有子类可见
protected void play(){
System.out.println("play...");
}
//所有类可见
public void sleep(){
System.out.println("sleep...");
}
}
非访问修饰符
- static:静态修饰符,修饰方法和变量。**静态变量独立于对象,无论这个类实例化多少个对象,这些对象共享这一个静态变量。**静态方法独立于对象,可以在类的其他方法中调用,不能使用类的非静态变量,也可以在类的外面使用类名直接调用,不需要实例化。
- final:常量修饰符,修饰类、方法和变量。修饰的类不能被继承,修饰的方法不能被继承类重新定义,修饰的变量不能被修改
- abstract:抽象修饰符,用来创建抽象类和抽象方法
- synchronized / volatile:用于线程的编程
public class ShoppingSystem{
//静态变量,所有实例共享
private static int personNum = 0; //系统在线人数
//构造方法
ShoppingSystem(){
personNum++;
}
//非静态方法
protected int getPersonNum(){
return personNum;
}
//静态方法
protected static String getStatus(){
return "ShoppingSystem is running...";
}
//常量变量
private static final String systemName = "ShoppingSystem";
//常量方法
public final void printSystemName(){
System.out.println(systemName);
}
}
public class OOPTest{
public static void main(String args[]){
ShoppingSystem system1 = new ShoppingSystem();
ShoppingSystem system2 = new ShoppingSystem();
System.out.println("person num: " + system1.getPersonNum());
System.out.println(ShoppingSystem.getStatus());
}
}
重载
在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。在对象调用重名方法时,会根据形参列表匹配正确的方法进行调用。
public class MyArrayUtil {
public static int getMax(int[] arr){}
public static int getMax(int[] arr1, int[] arr2){}
public static double getMax(double[] arr){}
}
封装
隐藏对象内部的复杂性,只对外公开简单的接口,便于外界调用,提高系统的可拓展性、可维护性。把该隐藏的隐藏,把该暴露的暴露,这就是封装的设计思想(高内聚,低耦合)。
最常用的封装就是JavaBean,即私有的属性,公有的方法
public class Student{
private String name;
private int age;
public String getName(){
return name;
}
public void setName(String Name){
name = Name;
}
public int getAge(){
return age;
}
public void setAge(int Age){
age = Age;
}
}
继承
子类继承父类,子类包含父类中的所有属性和方法
一个父类可以有多个子类,一个子类不能有多个父类(单继承和多层继承)
重写
子类对父类同名同参数的方法进行修改并覆盖,在执行时,子类的方法覆盖父类的方法
-
子类重写的方法修饰符权限不小于父类被重写的方法,且子类不能重写父类中的private修饰的方法(因为private的可见范围是同一类)
-
父类被重写方法的返回值是void,子类重写方法的返回值也必须是void
-
父类被重写方法的返回值是基本数据类型,子类重写方法的返回值也必须是相同的基本数据类型
-
父类重写方法的返回值是A类型,子类重写方法的返回值必须是A类或A的子类
-
子类重写的方法抛出的异常类型,与父类被重写的方法抛出的异常类型相同或为其子类
-
子类和父类中的同名同参数的方法要么都声明非static(是重写),要么都声明为static(不是重写)
super关键字
super代指父类,可以用来调用父类的属性、方法、构造器
- 当父类和子类中有同名属性时,可以在子类的方法或构造器中使用super调用父类的属性
- 当子类重写父类的方法时,可以在子类的方法中使用super调用父类的方法
- 在子类的构造器中可以显式使用super调用父类的构造器,且必须放在构造器的首行。若在子类构造器的首行没有显式声明 this() 或 super() ,则默认调用父类的空参构造器(此时若父类只定义了有参构造器,无参构造器就没了,如果在子类构造器首行没有显式声明this()或super()就会报错)
- 在类的构造器中对于 this() 和 super() 只能二选一
- 在类的多个构造器中,至少有一个类的构造器使用了super(),调用父类的构造器
public class Student extends Person{
private String major;
private int id = 1002;
private String school;
public Student(){}
public Student(String name, String major){
super(name); //显式调用父类构造器
this.major = major;
System.out.println("Student Constructor");
}
public void study(){
System.out.println("study...");
}
//重写
public void play(){
System.out.println("student play in the playground...");
}
public void printId(){
System.out.println(id);
System.out.println(super.id); //调用父类属性
}
public void playTest(){
play();
super.play(); //调用父类方法
}
}
子类对象实例化的过程
(1)从结果上来看,子类继承了父类所有的属性和方法。
(2)从过程上来看,子类调用构造器时,一定会直接或间接地调用父类的构造器,直到调用到顶层的Object类的构造器Object()。往内存中加载完所有父类的内容,子类才可以考虑调用父类的东西
(3)实例化完成后,即使调用了很多父类的构造器,但内存中只有一个对象(个人理解为调用父类构造器的本质只是把父类的属性和方法添加到子类中,并没有进行实例化),该对象包含子类及其所有父类的属性和方法。
关于Object类
如果一个类没有显示声明父类,则继承于Object类。
- Object类没有属性,构造器只有空参类型
- finalize():用于系统垃圾回收
- equals():比较两个对象是否相同
★ 多态
父类的引用指向子类的对象(父类的对象可以是子类的对象)
- 子类定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的只有等到方法调用的那一刻,编译器才确定调用的方法(多态是动态绑定,重载是静态绑定)。多态是运行时的行为,而不是编译时的行为。
- 属性不存在重写,是哪个类就用哪个类的属性
public class Person {
//所有类可见
public String appearance = "干净";
public void eat(){
System.out.println("一个人吃饭");
}
}
public class Man extends Person{
public boolean isSmoking;
public String appearance = "帅气";
public void earnMoney(){
System.out.println("赚钱");
}
public void eat(){
System.out.println("男生吃肉长肌肉");
}
public void walk(){
System.out.println("男生帅气地走路");
}
}
public class OOPTest {
public static void main(String[] args) {
//多态性:父类的引用指向子类的对象
Person p1 = new Man();
Person p2 = new Woman();
p1.eat(); //调用子类重写过的方法
// p1.walk(); //不能调用子类独有的方法
System.out.println(p1.appearance);//初始化的同名属性仍然是父类的属性
}
}
向下转型:强制类型转换,用于多态情况下的父类对象调用子类特有的属性和方法
//多态性:父类地引用指向子类的对象
Person p1 = new Man();
Person p2 = new Woman();
p1.eat(); //调用子类重写过的方法
// p1.walk(); //不能调用子类独有的方法
System.out.println(p1.appearance);//初始化的同名属性仍然是父类的属性
p1.name = "wanfeng";
//p1.isSmoking = false; //不能调用
Man m1 = (Man) p1;
m1.isSmoking = false; //向下转型后可调用
instanceof的使用:为了避免向下转型之前出现ClassCastException的异常,先用instanceof进行判断,如果为true就进行向下转型。若类型不匹配,编译可以通过,但运行就会报错
- 如果a instanceof A返回true,a instanceof B也返回true。那么A和B有继承关系
if(p1 instanceof Man){
Man m2 = (Man) p1;
m2.earnMoney();
})
常见问题:可以向下转型的条件是向下转型的类的结构在new 的时候就已经被加载进内存了
//编译通过,运行不通过
Person p3 = new Woman();
Man m3 = (Man) p3;
Person p4 = new Person();
Man m4 = (Man) p4;
//编译通过,运行也通过
Object o5 = new Woman();
Person p5 = (Person) o5;
★ 抽象
随着继承层次中许多子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类
抽象的语法
- **abstract修饰类时,该类不可实例化。**抽象类中一定有构造器,便于子类实例化时调用。开发中常会提供抽象类的子类,让子类实例化以完成相关需求
- abstract修饰方法时,该方法只声明方法名,没有方法体。当子类重写了父类中所有的抽象方法后,子类才能实例化,否则视子类为抽象类
- abstract不能修饰私有方法、静态方法或final修饰的方法,因为抽象的方法需要被子类重写
- 包含抽象方法的类一定是个抽象类(防止对象调用抽象方法)
public class AbstractTest {
public static void main(String[] args) {
//Person p1 = new Person(); 报错,抽象类不可实例化
//p1.eat();
Student s1 = new Student();
s1.walk();
//多态
Person p2 = new Student();
p2.walk();
//创建匿名子类的对象,和 Person p2 = new Student(); 是相同的性质
//只能用一次
Person p3 = new Person(){
@Override
public void walk(){
}
};
}
}
abstract class Person{
String name;
int age;
public Person(){
}
public Person(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println("Person eating...");
}
//抽象方法,只有方法声明,没有方法体,需要在子类重写
public abstract void walk();
}
class Student extends Person{
//子类重写父类的抽象方法
public void walk(){
System.out.println("Student walking...");
}
}
模板方法设计模式(TemplateMethod)
抽象类体现的是一种模板模式的设计,抽象类作为多个子类的通用模版,子类在抽象类的基础上扩展、改造,但子类总体上会保留抽象类的行为方式(即子类重写父类的抽象方法)
-
解决的问题:换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,步骤在父类中写好了,但是某些部分容易根据不同情况发生变化,就可以把这些步骤抽象出来,让子类去各自实现,这就是一种模板模式。
-
举例:计算程序运行时间的模版(父类),使用快速排序类(子类)去实现
import java.util.Arrays;
public class TemplateTest {
public static void main(String[] args) {
QuickSort quickSort = new QuickSort();
quickSort.run();
quickSort.spendTime();
}
}
abstract class Template{
public void spendTime(){
long start = System.currentTimeMillis();
run(); //抽象的步骤,会根据实际情况改变
long end = System.currentTimeMillis();
System.out.println("spend time: " + (end - start));
}
public abstract void run();
}
class QuickSort extends Template{
//实现(重写)Template中的抽象方法run
@Override
public void run() {
int[] arr = new int[10000];
for(int i=0; i<10000; i++){
arr[i] = (int) (Math.random()*100000);
}
System.out.println(Arrays.toString(arr));
Partition(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void Partition(int[] arr, int left, int right){
//划分左右区的算法,此处不再详细说明
}
}
★ 接口
接口的概念
在实际中,有时必须从几个类中派生出一个子类,继承所有的属性和方法,但是Java不支持多重继承,但是接口可以实现这一效果。
接口的本质是契约、标准、规范。继承是一个“是不是”的关系,而接口是一个“能不能”的关系。例如下图中学习是跨栏运动员和大学生两个类共有的行为,跨栏运动员和大学生是学习接口的实现类,实现了学习这个行为功能
-
一个类可以实现多个接口
-
类和接口是两个并列的结构
接口版本特性
JDK7及以前:接口中只能定义全局常量和抽象方法(修饰符可省略)
JDK8:可以定义全局常量、抽象方法、静态方法、默认方法
接口语法:JDK7
- 接口中不能定义构造器,接口不可实例化
- 接口需要通过类去实现,如果实现类实现了接口中的所有抽象方法,则该实现类就可以实例化,否则视该实现类为抽象类
- 一个类可以实现多个接口(接口的多实现)
- 接口和接口之间可以多继承,实现类需要实现接口及其所有继承关系的所有接口中的所有抽象方法
- 接口的使用体现了多态性
ppublic class InterfaceTest {
public static void main(String[] args) {
System.out.println(Study.MAX_SPEED);
System.out.println(Study.MIN_SPEED);
Student student = new Student();
student.run();
student.study();
student.careMood();
Monkey monkey = new Monkey();
monkey.stop();
//创建接口的非匿名实现类的匿名对象
new Student().run();
//创建接口的匿名实现类的非匿名对象
Love love = new Love() {
@Override
public void careMood() {
}
@Override
public void sendGift() {
}
};
//创建接口的匿名实现类的匿名对象
new Love(){
@Override
public void careMood() {
}
@Override
public void sendGift() {
}
}.careMood();
}
}
interface Do{
void do();
}
interface Study extends Do{
//全局常量 public static final修饰符可省略
public static final int MAX_SPEED = 7900;
int MIN_SPEED = 0;
//抽象方法 public abstract修饰符可省略
public abstract void run();
void stop();
void study();
}
interface Love{
void careMood(); //共情
void sendGift(); //送礼物
}
class Person{
private String name;
private int age;
//getter and setter
}
//Student类继承Person类,实现Study,Love两个接口
class Student extends Person implements Study, Love{
//实现接口的方法
@Override
public void run() {
System.out.println("Student run...");
}
@Override
public void stop() {
System.out.println("Student stop...");
}
@Override
public void study() {
System.out.println("Student study...");
}
@Override
public void careMood() {
System.out.println("Student care girlfriend's mood...");
}
@Override
public void sendGift() {
System.out.println("Student send a gift to girlfriend...");
}
@Override
public void do() {
System.out.println("Student do something...");
}
}
class Monkey implements Study{
@Override
public void run() {
System.out.println("Monkey run...");
}
@Override
public void stop() {
System.out.println("Monkey stop...");
}
@Override
public void study() {
System.out.println("Monkey study...");
}
@Override
public void do() {
System.out.println("Monkey do something");
}
}
这里放出结构图,理解起来更清晰
接口语法:JDK8
- 接口中定义的静态方法只能通过接口来调用
- 接口中定义的默认方法可以通过实现类对象调用
- 如果实现类重写了接口中的默认方法,执行时调用重写的方法
- 如果一个类的父类和实现的接口中有同名同参数的默认方法,且该类没有重写此方法,优先调用父类中的方法(类优先原则)
- 如果一个类实现的两个接口中有同名同参数的默认方法,且该类没有重写此方法,报错(接口冲突),若要使用必须在该类中重写此方法
- 若要在实现类重写了接口中的默认方法,仍然要调用接口中的方法,使用“接口.super.方法”调用
interface CompareA{
//静态方法 public可省略
public static void method1(){
System.out.println("Compare A method 1");;
}
//默认方法 public可省略
default void method2(){
System.out.println("Compare A method 2");
}
default void method3(){
System.out.println("Compare A method 3");
}
default void method4(){
System.out.println("Compare A method 4");
}
}
interface CompareB{
//default void method4(){
// System.out.println("Compare B method 4");
//}
//如果一个类实现的两个接口中有同名同参数的默认方法,且该类没有重写此方法,报错
}
class SuperClass{
public void method3(){
System.out.println("SuperClass method 3");
}
}
class SubClass extends SuperClass implements CompareA, CompareB{
//重写接口中的默认方法
public void method2(){
System.out.println("SubClass method 2");
}
public void myMethod(){
CompareA.super.method2(); //重写以后仍然调用接口中的默认方法
}
}
public class JDK8Test {
public static void main(String[] args) {
SubClass s = new SubClass();
s.method2(); //实现类的对象调用默认方法
//s.method1();
CompareA.method1(); //接口中定义的静态方法只能通过接口调用
s.method3();
//如果父类和实现的接口中有同名同参数的方法,且该类没有重写此方法,优先调用父类中的方法(类优先原则)
}
}
接口应用:代理模式(Proxy)
代理设计就是为其他对象提供一种代理以控制对这个对象的访问。从下面的代码来看,代理的过程就是将被代理的实现类对象server丢给代理的实现类proxyServer去执行server中重写的方法(此处简单体会逻辑就行,深入学习代理模式还需要后面的反射等知识)
public class NetWorkTest {
public static void main(String[] args) {
Server server = new Server();
ProxyServer proxyServer = new ProxyServer(server);
//用server对象作为参数构造ProxyServer对象,ProxyServer中的接口network就是server
proxyServer.browse();
}
}
interface Network{
void browse();
}
class Server implements Network{
@Override
public void browse() {
System.out.println("真实的服务器访问网络");
}
}
class ProxyServer implements Network{
private Network network;
public ProxyServer(Network network) {
this.network = network;
}
public void check(){
System.out.println("检查工作");
}
@Override
public void browse() {
check();
network.browse();
}
}
接口应用:工厂模式(Factory)
实现了创建者与调用者的分离,即把创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的
- 缺点:增加新情况时需要修改代码,违反开闭原则(对扩展开放,对修改封闭)
//简单工厂模式
interface Car{
void run();
}
class BMW implements Car{
@Override
public void run() {
System.out.println("宝马行驶");
}
}
class Porsche implements Car{
@Override
public void run() {
System.out.println("保时捷行驶");
}
}
//分离出创建对象的过程
class CarFactory{
//方式一
public static Car getBMW(){
return new BMW();
}
public static Car getPorsche(){
return new Porsche();
}
//方式二
public static Car getCar(String car_type){
if(car_type.equals("BMW")){
return new BMW();
}else if(car_type.equals("Porsche")){
return new Porsche();
}else{
return null;
}
}
}
public class FactoryTest {
public static void main(String[] args) {
Car b = CarFactory.getBMW();
Car p = CarFactory.getCar("Porsche");
if(b!=null) b.run();
if(p!=null) p.run();
}
}
内部类
如果一个事物的内部,还需要一个完整的结构去描述,而这个内部结构又只为这个事物服务,那么这个内部结构就可以使用内部类(例如一个人的心脏就是一个内部结构,包含心室血管等,而心脏只能为人这个类去服务,不能给其他物种服务,人的心脏就可以定义为人的内部类)
内部类的分类:成员内部类(定义在类中作为成员),局部内部类(定义在方法中)
内部类:语法
- 内部类作为一个类,可以定义构造器、属性、方法等,可以被static修饰,可以被final修饰而不可继承,可以被abstract修饰而不能实例化。内部类作为成员,可以调用外部类的属性、方法,可以被四种不同的访问修饰符修饰
- 非静态成员内部类可以调用外部类静态和非静态的方法。静态成员内部类不能调用外部类的非静态方法
- 内部类调用外部类的结构:“外部类.this.属性/方法”
- 开发中常见的内部类的使用:内部类用于实现接口,并返回该内部类的对象(返回类型是接口)
package oop.InnerClass;
public class InnerClassTest {
public static void main(String[] args) {
//创建静态内部类实例
Person.Brain brain = new Person.Brain();
brain.think();
//创建非静态内部类实例(需要外部类实例)
Person person = new Person();
Person.Heart heart = person.new Heart();
heart.jump();
heart.show(0.00);
}
}
class Person{
private double weight = 60.00;
public void live(){
//局部内部类
class Meat{
String name;
public void cook(){
System.out.println("Meat cooking");
System.out.println(Person.this.weight); //调用外部类的结构
method1();
method2();
}
}
}
//非静态成员内部类
class Heart{
private double weight = 4.00;
public void jump(){
System.out.println("Heart jumping");
method1(); // Person.this.method1();
method2(); //非静态成员内部类可以调用外部类静态和非静态的方法
}
public void show(double weight){
System.out.println(weight); //形参
System.out.println(this.weight); //内部类的属性
System.out.println(Person.this.weight); //外部类的属性
}
}
//静态成员内部类
static class Brain{
private double weight;
public void think(){
System.out.println("Brain thinking");
//method1(); //静态成员内部类不能调用外部类的非静态方法
method2();
}
}
public void method1(){
System.out.println("Person method 1");
}
public static void method2(){
System.out.println("Person method 2: static");
}
//开发中常见的内部类的使用:内部类用于实现接口,并返回该内部类的对象(返回类型是接口)
public Study getStudyMethod(){
class StudyMethod implements Study{
@Override
public void study() {
System.out.println("this is my study method");
}
}
return new StudyMethod();
}
}
interface Study{
void study();
}
匿名对象
public class OOPTest {
public static void main(String[] args) {
//匿名对象
new Person().play();
new Person().sleep();
}
}
this关键字
this用于指定当前对象,当属性和形参重名时,用于区分属性和形参。通常在开发时省略该关键字
public class Person{
private String name;
public void setName(String name){
this.name = name;
}
}
this调用构造器,this调用构造器只能写在第一行(因为只有构造出对象才能进行其他操作)
public class Animal {
private String name;
private int age;
public Animal(){
System.out.println(" Animal() exec ");
}
public Animal(String name){
this();
this.name = name;
System.out.println(" Animal(String) exec ");
}
public Animal(String name, int age){
this(name);
this.name = name;
this.age = age;
System.out.println(" Animal(String, int) exec ");
}
}
public class OOPTest {
public static void main(String[] args) {
Animal animal = new Animal("jingyu", 21);
}
}
可变个数的形参
形参个数可以自定义。如下代码中,传入的strs是数组名
可变个数形参必须声明在参数列表的末尾
public class MyUtil {
//可变个数的形参
public static void show(String ... strs){
System.out.println(strs);
System.out.println(strs[0]);
}
public static void show(int num, String ... strs){
}
}
public class OOPTest {
public static void main(String[] args) {
MyUtil.show("wanfeng", "jingyu");
}
}
方法的参数的值传递机制
- 基本数据类型:传入的参数是另一份拷贝,对原来的变量不造成任何影响
- 引用数据类型:传入的参数也是拷贝,但是传入的是地址值,相同地址值必定找到同一片存储空间,修改也会对原来的变量造成影响。
- String类型比较特殊,虽是引用数据类型,但是String是常量,在创建以后不可修改,当对String类型变量修改时已经进行了new String(),所以打印出来的地址会不一样,原来的String变量也不改变
public static void changeInt(int num){
num += 10;
}
public static void changePerson(Person p){
p.setName("jingyu");
}
public static void changeString(String str){
str = str.toUpperCase();
System.out.println("str传入后的地址:"+Integer.toHexString(System.identityHashCode(str)));
}
public static void main(String args[]){
//基本数据类型
int num = 10;
changeInt(num);
System.out.println("num: " + num);
//引用数据类型
Person p = new Person();
p.setName("wanfeng");
changePerson(p);
System.out.println("p.name: " + p.getName());
//String
String str = "aBcD";
System.out.println("str传入前的地址: " + Integer.toHexString(System.identityHashCode(str)));
changeString(str);
System.out.println("str: " + str);
}
对象数组的内存解析
public class Student{
private String name;
private int age;
private boolean isMale;
}
public class StudentTest(){
public static void main(String args[]){
Student[] stus = new Student[3]; //初始化
stus[0] = new Student[];
}
}
面向对象例题(加深理解)
【例题1】假装考方法的参数传递
public class Test{
public static void main(String args[]){
int a=10;
int b=10;
method(a, b);
//在method方法调用之后,仅打印出a=100, b=200。请写出method的代码
Sytem.out.println("a="+a);
Sytem.out.println("b="+a);
}
}
//方法一:在method方法中打印完成直接退出程序
public static void method(int a, int b){
a *= 10;
b *= 20;
System.out.println("a="+a);
System.out.println("b="+b);
System.exit(0);
}
//方法二:重写打印流中的println方法
public static void method(int a, int b){
PrintStream ps = new PrintStream(System.out){
@Override
public void println(String x){
if("a=10".equals(x)){
x = "a=100";
}else if("b=10".equals(x)){
x = "b=200";
}
super.println(x);
}
};
System.setOut(ps);
}
【例题2】数组计算
public class Test{
public static void main(String args[]){
int[] arr = new int[]{12, 3, 3, 34, 56, 77, 432};
//让数组的每个位置的值去除以首位置的元素得到的结果作为该位置的新值
}
}
//错误写法
for(int i=0; i<arr.length; i++){
arr[i] = arr[i] / arr[0];
}
//正确写法
for(int i=arr.length-1; i>=0; i--){
arr[i] = arr[i] / arr[0];
}
【例题3】字符数组
char[] arr1 = new char[10];
System.out.println(arr1);
//输出内容是10个'\0',而不是地址值。
//这是println方法对于字符型数组的重载,相当于shu'z
【例题4】多态和向下转型
public class OOPPrac1 {
public static void main(String[] args) {
Base1 base = new Sub1();
base.add(1, 2, 3);
//多态,子类重写的方法覆盖父类被重写的方法,输出sub1
Sub1 s = (Sub1) base;
s.add(1, 2, 3);
//重载,确定的形参列表的方法优先被调用,输出sub2
}
}
class Base1{
public void add(int a, int... arr){
System.out.println("base1");
}
}
class Sub1 extends Base1{
public void add(int a, int[] arr){
System.out.println("sub1");
}
public void add(int a, int b, int c){
System.out.println("sub2");
}
}
【例题5】== 和 equals的区别
- == 可以使用在基本数据类型和引用数据类型中。如果是基本数据类型则比较值是否相等,如果是引用数据类型则比较地址值是否相等
- equals()适用于引用数据类型。若调用Object类中的equals,返回的是==运算符的比较结果(比较地址值)。某些特殊的引用数据类型如String、Date、File、包装类等重写了equals方法,会去比较内容。由此得出可以在自定义的类中重写equals方法用来比较需要的内容
【例题6】抽象类和接口有哪些异同?
- 在语法上,从实例化、继承、构造方法、成员变量、方法、类和接口之间的三种关系解释。
- 抽象类和接口都不能实例化,都可以被继承。
- 抽象类有构造器,接口没有构造器。
- 抽象类可以定义很多种变量,接口只能定义静态常量。
- 抽象类的方法可以是抽象和非抽象的,接口的方法在jdk7以前只能是抽象的,jdk8以后可以有非抽象的默认方法和静态方法
- 类和类之间只能单继承,可以多层继承,接口和接口可以单继承和多继承
- 在思想上,抽象类定义的是一个继承体系中的共性内容,而接口是功能的集合,是一个体系额外的功能,是暴露出来的规则。换句话来说,接口是对动作的抽象,抽象类是对根源的抽象
【例题7】接口题排错
interface A{
int x = 0;
}
class B {
int x = 1;
}
class C extends B implements A{
public void pX(){
System.out.println(x); //报错,变量名模糊
System.out.println(A.x); // 0 接口定义的变量是全局变量
System.out.println(super.x); // 1 父类的变量
}
public static void main(String args[]){
new C().pX();
}
}
interface Playable{
void play();
}
interface Bounceable{
void play();
}
interface Rollable extends Playable, Bounceable{
Ball ball = new Ball("PingPang");
}
class Ball implements Rollable{
private String name;
public Ball(String name) {
this.name = name;
}
public String getName(){
return this.name;
}
@Override
public void play() {
ball = new Ball("Football"); //ball在接口中定义为常量,不可再次被修改
System.out.println(ball.getName());
}
}
四、异常处理
异常的概述与体系结构
在开发中,代码写得尽善尽美,在系统运行过程仍然会遇到一些问题,不能靠代码避免的问题,比如客户输入数据的格式,读取文件是否存在,网络是否通常
异常:程序执行中不正常的情况称为异常
- Error:JVM无法解决的严重问题,如内部错误、资源耗尽,一般不去编写代码针对解决
- Exception:其他的因编程错误或偶然的外在因素导致的一般性问题,称为异常。例如空指针访问、数组越界、网络断开。捕获异常最理想的是在编译期间,但有些错误只有在运行时才会发生
常见的异常
编译时异常
- IOException:IO异常
- FileNotFoundException:文件未找到异常
运行时异常
- NullPointerException:空指针异常,调用为空的引用数据类型时出现
- IndexOutOfBoundsException:下标越界
- ClassCastException:类型转换异常
- NumberFormatException:数值格式异常(例如字母的字符串转换为Integer类型)
- InputMismatchException:输入不匹配异常
- ArithmeticException:算术异常(例如一个数除以0)
异常处理机制:try-catch-finally
Java出现异常处理机制的意义是增强代码的可读性,避免代码过长臃肿,使程序简洁,优雅,易于维护。
try-catch的处理过程是代码段自己处理解决异常。当出现的异常匹配到某一个catch时,执行catch中代码段执行,执行完成后跳出try-catch结构
- catch的异常类型的顺序必须是子类在上,父类在下
- 在try中声明的变量是局部变量
- finally是可写可不写的,其中的代码是一定会被执行的,即使try和catch中有return或者又出现了新的未处理的异常。通常在finally中释放数据库连接、输入输出流、网络编程Socket等资源
try {
//可能出现异常的代码段
String str = "123";
str = "wanfeng";
int num = Integer.parseInt(str);
System.out.println("Integer.parseInt");
}catch (NumberFormatException e) {
// 异常处理,常见处理方式有 String getMessage() , printStackTrace()
System.out.println("catch: 数值转换异常,请检查");
int[] arr = new int[3];
System.out.println(arr[3]); //未处理的异常
}catch (NullPointerException e) {
System.out.println(e.getMessage());
}catch (Exception e){
e.printStackTrace();
}finally{
//异常处理后一定会执行的代码段
System.out.println("finally: 代码段执行");
}
异常处理机制:throws
程序在正常执行过程中,一旦出现异常,就会在异常代码处生成对应异常类的对象并将此对象抛出给上一级(调用者),抛出对象后剩余代码不再执行
public class ExceptionTest2 {
//throws 抛出异常给调用者method1()
public static void run1() throws NumberFormatException{
//可能出现异常的代码段
String str = "123";
str = "wanfeng";
int num = Integer.parseInt(str);
// throws抛出异常后,剩余的代码段不再执行
System.out.println("Integer.parseInt");
}
//抛出异常给调用者main()
public static void method1() throws NumberFormatException{
run1();
}
//处理异常,
public static void main(String[] args){
try {
method1();
}catch (NumberFormatException e){
System.out.println("catch NumberFormatException");
e.printStackTrace();
}
}
}
方法重写抛出异常的规则
-
子类重写的方法抛出的异常类型A不大于父类被重写方法抛出的异常类型B(A是B的子类或同类)
-
父类没有抛异常,子类也不能抛
import java.io.FileNotFoundException;
import java.io.IOException;
// 重写方法异常抛出的规则
public class ExceptionTest3 {
public static void main(String[] args) {
ExceptionTest3 t3 = new ExceptionTest3();
t3.display(new Student());
/**
* 多态
* 此时执行的是Student重写的方法,抛出FileNotFoundException
* 该异常类型是IOException的子类可以被catch到,如果是Exception
* 就catch不到了
*/
}
public void display(Person person){
try {
person.method();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Person{
public void method() throws IOException {
}
}
class Student extends Person{
public void method() throws FileNotFoundException {
}
}
开发中异常处理方式的选择
- 如果父类被重写的方法没有throws,则子类重写方法也不能throws,若有异常必须用try-catch处理
- 在执行方法M中又先后调用了另外几个方法,这几个方法是递进执行的。通常在里面的几个方法抛出异常,在M中进行try-catch统一处理
手动抛出异常对象
通常异常对象由系统自动生成,但我们也可以手动生成并抛出
public class ExceptionTest4 {
public static void main(String[] args) {
User user = new User();
try {
user.register(-1001);
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
System.out.println(user);
}
}
class User{
private int id;
// 方法名后也要加throws
public void register(int id) throws Exception {
if(id > 0){
this.id = id;
}else{
System.out.println("输入数据异常");
// 手动抛出异常
throw new Exception("数据输入异常,请检查");
}
}
@Override
public String toString() {
return "User{" +
"id=" + id +
'}';
}
}
自定义异常
- 继承现有的异常类
- 提供全局常量 serialVersionUID (序列化ID,类的唯一标识)
- 提供重载构造器
- 使用时手动抛出自定义的异常
import java.util.Scanner;
//自定义异常
public class ExceptionTest5 {
public static void run1() throws DataInputIllegalException{
Scanner scanner = new Scanner(System.in);
System.out.println("input a number: ");
int num = scanner.nextInt();
if(num < 0){
throw new DataInputIllegalException("输入数据非法");
}
}
public static void main(String[] args) {
try {
run1();
}catch (DataInputIllegalException e){
e.printStackTrace();
}
}
}
// 数据输入不合法异常
class DataInputIllegalException extends RuntimeException{
static final long serialVersionUID = -7034897190885766939L;
public DataInputIllegalException(){
}
public DataInputIllegalException(String message){
super(message); //调用父类构造器
}
}
五、多线程
单核CPU和多核CPU
- 单核CPU是一种假的多线程,因为在同一时刻一个核只能运行一个任务。
- 多核才能更好地发挥多线程的效率
并行与并发
- 并行:多个CPU(核)同时执行多个任务
- 并发:一个CPU(核)同时执行多个任务(本质是以时间片为单位在多个任务之间来回切换,只不过CPU的主频很高看起来是在同时执行)
使用多线程的优点
- 提高应用程序响应,增加用户体验
- 提高计算机系统CPU的利用率
- 改善程序结构(将长而复杂的进程分为多个线程,独立运行,利于理解和修改)
使用多线程的场景
- 程序需要同时执行多个任务
- 程序需要实现一些需要等待的任务,如用户输入、文件读写、网络操作
- 需要后台运行的程序
创建多线程的方式一:继承Thread类
- 声明一个子类,继承于java.lang.Thread类
- 重写run方法(线程执行的代码)
- 调用start方法开启线程,而不是直接调用run方法
- 多个线程的创建则需要多个对象
public class ThreadTest1 {
public static void main(String[] args) {
//非匿名子类
MyThread t1 = new MyThread();
t1.start();
// 匿名子类
Thread t2 = new Thread(){
@Override
public void run() {
for(int i=0; i<1000; i++){
System.out.println(i + " t2.run: 执行成功~");
}
}
};
Thread t3 = new Thread(){
@Override
public void run() {
for(int i=0; i<1000; i++){
System.out.println(" " + i + " t3.run: 执行成功~");
}
}
};
t2.start();
t3.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println(i + " MyThread.run: 执行成功~");
}
}
}
Thread类的常用方法
- run():线程体
- start():开启线程
- Thread.currentThread():返回执行当前代码的进程(静态方法,通过类名调用)
- getName() / setName():获取 / 设置线程名称
- Thread(String name):构造器(线程名)
- yield():释放该线程对CPU的占用
- join():在线程A中调用线程B的join方法,线程B抢占CPU,线程A阻塞,直到线程B执行完后释放CPU
- stop():强制结束线程,不建议使用(在源码中已废弃)
- sleep(long millitime):线程睡眠(阻塞)一段时间(单位毫秒)
- isAlive():判断当前线程是否存活
线程优先级的设置
Java中的线程调度策略是,同优先级的线程先到先服务,使用时间片轮转。对于较高优先级的线程使用抢占式的优先调度。
优先级等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
方法
- getPriority() / setPriority(int priority):获取 / 设置进程优先级
注意点
-
线程创建时继承父线程的优先级
-
低优先级的线程只是被调度的概率低,并非一定是等到高优先级线程执行完后才被调度
案例:多窗口卖票
public class TicketSell {
public static void main(String[] args) {
for(int win=0; win<3; win++){
new SellThread("窗口"+win).start();
}
}
}
class SellThread extends Thread{
//静态变量
private static int ticketNum = 10;
public SellThread(String name){
setName(name);
}
@Override
public void run() {
while(ticketNum > 0){
try {
//模拟等下一个人来的等待时间,随机睡眠 0-10秒
sleep((long) (Math.random()*10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//票卖出,票数减一
//这里要再做一次判断的原因是,有一种情况是,只剩一张票了,但是有三个线程都在睡眠等待
//如果不加这个判断条件,卖出三张票,ticket变为-2
if(ticketNum > 0) ticketNum--;
else{
System.out.println(getName()+ ": 票已经卖完喽,抱歉,明天再来吧~");
break;
}
System.out.println(getName()+ " 售卖成功,当前剩余票数: "+ticketNum);
}
}
}
创建多线程的方式二:实现Runnable接口
- 创建一个实现类,实现Runnable接口
- 实现Runnable中的抽象方法run()
- 创建实现类的对象
- 将该对象作为Thread类构造器参数,创建Thread对象
- 调用Thread类对象的start方法开启线程
public class ThreadTest5 {
public static void main(String[] args) {
MyRunnable5 r5 = new MyRunnable5();
new Thread(r5).start();
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
for(int i=0; i<100; i++){
if(i%3==0) System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
开发中优先选择实现Runnable接口的方式创建多线程。
继承Thread的方式有单继承的局限性,而且实现Runnable接口的方式更适合多个线程共享数据的情况
两种方式都需要重写run方法
线程的生命周期
- 新建:当一个Thread类或其子类的对象被声明创建时,线程处于新建状态
- 就绪:线程调用start方法后,等待被CPU调度
- 运行:获取到所有资源并被CPU调度后进入运行状态
- 阻塞:在特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止运行,进入阻塞状态
- 死亡:线程完成了所有工作或被提前强制性终止
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ava2swZU-1681975786644)(D:\Typora笔记\Java基础\语法基础img\线程生命周期.jpg)]
线程的同步
有时,多个线程对共享数据的访问会造成操作的不完整性(即多个线程对共享数据同时操作就会引起数据不安全)。例如三个窗口卖票,当只剩一张票时,可能会有三个线程同时进入,导致线程执行完后票数出现负数的情况
使用synchronized关键字解决
(1)同步代码块。synchronized能保证在同一时刻只有一个线程进入该代码块。同步监视器又称锁,任何一个类的对象都可以作为锁(在实现Runnable接口的情况下用this最方便,在继承Thread类时可以用 当前类.class 作为锁),因此所有线程要共用一把锁,才能保证线程的同步。但这种方式相当于单线程,效率较低
synchronized(同步监视器){
//需要被同步的代码段(共享段,在操作系统原理上被称为临界区)
}
(2)同步方法。如果访问共享数据的代码都在一个方法中,可以将这个方法声明为同步方法。同步监视器不需要显示声明,在非静态同步方法中同步监视器是 this,在静态方法中是 当前类.class(在继承Thread方式中使用同步方法一定要使用静态的同步方法)
class MyThread6 extends Thread{
@Override
public void run() {
}
//同步监视器为 MyThread6.class
public static synchronized void method1(){
//...
}
//同步监视器为 this
public synchronized void method2(){
//...
}
}
使用同步机制将单例模式中的懒汉式改写为线程安全的
class Bank{
private Bank(){
}
private static Bank instance = null;
方式一:效率较差
//public static Bank getInstance(){
// synchronized(Bank.class){
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
//}
//方式二:只有当没有实例时线程才进入同步代码块,否则直接返回实例
public static Bank getInstance(){
if(instance == null){
synchronized(Bank.class){
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
使用Lock解决
- 创建ReentrantLock对象lock
- 在执行临界区代码之前上锁,执行lock.lock()
- 执行完临界区代码之后解锁,执行lock.unlock()。最好使用try-finally包裹,保证lock.unlock()一定会被执行到,以避免出现死锁
class SellRun implements Runnable{
private int ticketNum = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
//上锁
lock.lock();
if(ticketNum>0){
ticketNum--;
System.out.println(Thread.currentThread().getName()+": 售票成功,剩余票数为 "+ticketNum);
}
else{
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
synchronized和Lock的异同和比较
- 相同点:二者都可以解决线程安全问题
- 不同:synchronized在执行完临界区代码时自动上锁和释放,而Lock需要手动上锁和手动释放
- 优先使用顺序(灵活性):Lock > 同步代码块 > 同步方法
线程通信
- wait():线程阻塞并释放锁
- notify():唤醒阻塞的一个线程(优先级高的)
- notifyAll():唤醒所有阻塞的线程
三个方法必须使用在同步代码块或同步方法中,且三个方法的调用者必须是同步代码块或同步方法中的同步监视器
案例:两个线程交替打印1-100
class NumberRun implements Runnable{
private int number = 1;
@Override
public void run() {
while(true){
synchronized (this) {
notify(); //唤醒阻塞线程
if(number <= 50){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": "+number);
number++;
try {
wait(); //阻塞进程,并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class ThreadTest8 {
public static void main(String[] args) {
NumberRun numberRun = new NumberRun();
for(int i=0; i<2; i++){
new Thread(numberRun).start();
}
}
}
sleep()和wait()方法的异同
- 相同:使当前线程进入阻塞状态
- 不同:两个方法声明位置不同,sleep定义在Thread类中,wait()方法定义在Object类中。调用要求不同,sleep可以在任何场景下调用,wait必须在同步代码块或同步方法中调用。如果两个方法都使用在同步代码块或同步方法中,wait会释放锁,而sleep不会释放锁。
创建多线程的方式三:实现Callable接口
特点
- 线程体可以有返回值
- 方法可以抛出异常
- 支持泛型返回值
- 需要借助FutureTask类,比如获取返回结果
创建方法
- 实现Callable方法,实现call方法(线程执行体)
- 将Callable实现类对象作为FutureTask构造器参数,创建FutureTask对象
- 将FutureTask对象作为Thread构造器参数,开启线程
- 可以通过FutureTask中的get方法获取call方法的返回值
public class ThreadTest9 {
public static void main(String[] args) {
//Callable实现类对象
MyCall myCall = new MyCall();
// 将Callable实现类对象作为FutureTask构造器参数
FutureTask futureTask = new FutureTask(myCall);
//以futureTask作为Thread构造器参数,并开启线程
new Thread(futureTask).start();
try {
//get方法的返回值为 FutureTask构造器参数Callable实现类的对象重写的call方法返回值
Object res = futureTask.get();
System.out.println("sum = " + res);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCall implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for(int i=0; i<100; i++){
if(i%3==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
创建多线程的方式四:使用线程池
特点:提前创建好多个线程,放如线程池中,使用时直接获取,使用完后返回池中。可以避免频繁创建销毁、实现重复利用
优点
- 提高响应速度(减少创建新线程的时间)
- 降低资源消耗(线程重复利用)
- 便于线程管理
线程池属性
- corePoolSize:核心池大小
- maximumPoolSize:最大线程数
- keepAliveTime:没有线程执行任务时线程池的最大保留时间
使用
- 使用Executors工厂类创建一个固定数量的线程池,返回值类型为ExecutorService(线程池对象)
- 设置线程池属性,因为ExecutorService是接口,需要强转成ThreadPoolExecutor类
- 调用execute(Runnable r)开启线程(适用于Runnable接口),或调用submit(Callable c)开启线程(适用于Callable接口)
- 使用结束后调用shutdown()关闭线程池
public class ThreadTest10 {
public static void main(String[] args) {
//使用Executors工厂类创建一个固定数量的线程池,返回值类型为ExecutorService
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池属性,因为ExecutorService是接口,需要强转成ThreadPoolExecutor类的
ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) service;
poolExecutor.setCorePoolSize(15);
//开启一个线程 适用于Runnable
poolExecutor.execute(new NumberRun10());
//开启一个线程 适用于Callable
poolExecutor.submit(new NumberCall10());
//关闭线程池
poolExecutor.shutdown();
}
}
class NumberRun10 implements Runnable{
@Override
public void run() {
for(int i=0; i<100; i++){
if(i%2 == 0) System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
class NumberCall10 implements Callable{
@Override
public Object call() throws Exception {
for(int i=0; i<100; i++){
if(i%2 == 1) System.out.println(Thread.currentThread().getName()+": "+i);
}
return null;
}
}
六、枚举
类的对象只有有限个,确定的,这样的类是枚举类。当需要定义一组常量时建议用枚举类.
自定义枚举类
public class EnumTest {
@Test
public void test1(){
Season spring = Season.SPRING;
Season winter = Season.WINTER;
System.out.println(spring);
System.out.println(winter);
}
}
//自定义枚举类
class Season{
//属性为私有常量
private final String name;
//私有化类的构造器
private Season(String name){
this.name = name;
}
//提供当前枚举类的多个对象 公有静态常量
public static final Season SPRING = new Season("春天");
public static final Season SUMMER = new Season("夏天");
public static final Season AUTUMN = new Season("秋天");
public static final Season WINTER = new Season("冬天");
//可以提供 getter, toString
public String getName() {
return name;
}
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
'}';
}
}
使用enum关键字定义枚举类(JDK5之后)
- 使用enum关键字定义枚举类,默认继承于java.lang.Enum类
- 提供枚举类的多个对象,用逗号隔开,分号结束
- 提供的对象必须声明在最前面,不能加修饰符
enum ThreadStatus{
NEW("创建"),
READY("就绪"),
RUN("运行"),
BLOCK("阻塞"),
DEAD("死亡");
private final String description;
private ThreadStatus(String desc){
this.description = desc;
}
@Override
public String toString() {
return "ThreadStatus{" +
"description='" + description + '\'' +
"} " + super.toString();
}
}
Enum类的主要方法
-
toString():返回对象名
-
values():返回枚举类所有对象的数组
-
valueOf(String objectName):根据对象名返回枚举类对象,若没有则抛异常
@Test
public void test3(){
// toString() 返回对象名
ThreadStatus run = ThreadStatus.RUN;
System.out.println(run.toString());
// values()
ThreadStatus[] values = ThreadStatus.values();
for(int i=0; i<values.length; i++){
System.out.println(values[i]);
}
// valueOf(String objectName)
ThreadStatus block = ThreadStatus.valueOf("BLOCK");
System.out.println(block);
}
实现接口的枚举类
enum ThreadStatus implements Info{
//可以在每个枚举类对象中各自实现,也可以在类中统一实现
NEW("创建"){
@Override
public void show() {
System.out.println("show new status");
}
},
READY("就绪"){
@Override
public void show() {
System.out.println("show ready status");
}
},
RUN("运行"){
@Override
public void show() {
System.out.println("show run status");
}
},
BLOCK("阻塞"){
@Override
public void show() {
System.out.println("show block status");
}
},
DEAD("死亡"){
@Override
public void show() {
System.out.println("show dead status");
}
};
private final String description;
private ThreadStatus(String desc){
this.description = desc;
}
@Override
public String toString() {
return super.toString();
}
}
七、注解(Annotation)
注解是Java对元数据的支持,是代码里的特殊标记。通过注解,开发人员可以在不改变原有逻辑的情况下,对源文件嵌入一些补充信息,代码分析工具、开发工具等可以通过这些补充信息进行验证或部署。在JavaSE中,通常用于标记过时的功能、忽略警告等
在编译时进行格式检查(JDK内置的三个注解)
- @Override:限定重写父类方法或实现接口方法,只能用于方法
- @Deprecated:表示已过时,修饰类、方法等
- @SuppressWarning:抑制警告
自定义注解
自定义注解需要配上信息处理流程才有意义
public @interface MyAnnotation {
/*
成员变量以无参数方法来声明,可以使用default指定默认值
如果注解没有成员变量则作为标识
如果注解有成员需要再使用时指定值或设置默认值
*/
String value() default "wanwan";
}
元注解
对现有的注解进行解释说明的注解
@Retention:用于指定该Annotation的生命周期,@Retention包含一个RetentionPolicy类型的成员变量
- Retention.SOURCE:在源文件中有效
- Retention.CLASS:在class文件中有效
- Retention.RUNTIME:在运行时有效(可以通过反射获取)
@Target:用于修饰Annotation的定义,指定Annotation能修饰哪些元素
@Documented:用于指定Annotation类将被javadoc工具提取成文档(保留下来)
@Inherited:指定Annotation具有继承性(用该Annotation修饰类后,其子类也会被该Annotation修饰)
可重复注解(JDK8)
对Annotation使用 @Repeatable(AnnotationName.class) 修饰,该Annotation可以重复使用(Retention生命周期、Target、Inherited要相同)
类型注解(JDK8)
元注解@Target新增的两个ElementType枚举值
- TYPE_PARAMETER:表示该注解能写在类型变量的声明语句中
- TYPE_USE:表示该注解能写在使用类型的任何语句中
八、泛型
使用泛型的意义:集合容器类在声明阶段不确定到底实际存储的是什么类型的元素,JDK1.5之前只能把元素类型设计为Object,之后使用泛型解决。把元素的类型作为一个参数,这个参数就叫做泛型。
- 泛型必须是一个类,不能使用基本数据类型
- 在实例化时指明泛型的类型后,对象内部结构使用泛型时都使用实例化时指明的类型
九、反射
反射(Reflection)被视为动态语言的关键,所谓动态语言就是在运行时可以根据某些条件改变代码自身的结构。反射机制允许程序在执行期间借助Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性和方法
加载完类后,在堆内存的方法区产生了一个Class类型的对象(一个类只有一个Class对象,Class类型的对象用于存放某个类中定义的属性和方法),这个对象包含了完整的类的结构信息,我们可以通过这个对象看到类的结构,这个过程就称作反射(实例化的逆过程)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aj0AYkhm-1681975786645)(D:\Typora笔记\Java基础\语法基础img\反射的过程.jpg)]
反射的使用
public class ReflectionTest1 {
//反射方式
@Test
public void test2() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//通过反射,创建Person类的对象
Class pclass = Person.class;
Constructor constructor = pclass.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("wanfeng", 20);
Person p = (Person) obj;
System.out.println(p.toString());
//调用公有属性
Field age = pclass.getDeclaredField("age");
age.set(p, 13);
System.out.println(p.toString());
//调用公有方法
Method showAge = pclass.getDeclaredMethod("showAge");
showAge.invoke(p);
//调用私有构造器,并实例化
Constructor constructor1 = pclass.getDeclaredConstructor(String.class); //构造器类的对象,参数为数据类型的Class实例
constructor1.setAccessible(true); //私有结构需要设置可访问权限
Object w = constructor1.newInstance("wanwan"); //构造器类的对象调用newInstance实例化对象,参数和对应的构造器相同
Person pw = (Person) w;
System.out.println(pw);
//调用私有属性
Field name = pclass.getDeclaredField("name");
name.setAccessible(true);
name.set(pw, "fengfeng");
System.out.println(pw);
//调用私有方法
Method showName = pclass.getDeclaredMethod("showName");
showName.setAccessible(true);
int showNameRes = (int) showName.invoke(pw);
System.out.println("showName 方法的返回值 "+showNameRes);
}
}
class Person{
private String name;
public int age;
public Person() {
}
private Person(String name){
this.name = name;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"names='" + name + '\'' +
", age=" + age +
'}';
}
public void showAge(){
System.out.println("my age is "+age);
}
private int showName(){
System.out.println("my name is "+name);
return 32;
}
}
关于Class类和ClassLoader
执行javac命令后,生成一个或多个字节码文件(.class),每个字节码文件对应一个类。执行java命令对某个字节码文件解释运行,即把字节码文件加载进内存,这就是类的加载机制。加载进内存中的类称为运行时类,此运行时类,就是Class类的一个实例。
如果程序主动使用某一个类但是这个类还没被加载进内存中。(1)类的加载:类加载器将类的class文件读入内存并创建一个Class对象。(2)类的链接:将类的二进制数据合并到JRE中。(3)类的初始化:JVM负责对类初始化
运行时类会缓存一段时间,在此时间内可以通过不同的方式来获取此运行时类
// 获取Class的实例
@Test
public void test4() throws ClassNotFoundException {
// 1. 调用运行时类的属性 class
Class pclass = Person.class;
System.out.println(pclass);
// 2. 通过运行时类的对象调用getClass()
Person p1 = new Person();
Class pclass2 = p1.getClass();
System.out.println(pclass2);
// 3. 调用Class的静态方法 Class.forName(String path) path: 包名.类名
Class pclass3 = Class.forName("Reflection.Person");
System.out.println(pclass3);
// 4. 使用类的加载器ClassLoader
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
Class pclass4 = classLoader.loadClass("Reflection.Person");
System.out.println(pclass4);
}
使用Class类创建运行时类的对象
调用newInstance()创建对应的运行时类的对象,内部是调用了运行时类的空参构造器
- 运行时类必须提供空参构造器
- 空参构造器的访问权限足够,通常为public
@Test
public void test6() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class pclass = Class.forName("Reflection.Person");
Object object =pclass.newInstance();
Person person = (Person) object;
System.out.println(person);
}
获取运行时类的内部结构
@Test
public void test8(){
Class pclass = Person.class;
// getFields() 获取自己类及所有父类的public属性
Field[] fields = pclass.getFields();
for(Field f : fields){
System.out.println(f);
}
// getDeclaredFields() 获取自己类声明的所有属性
Field[] declaredFields = pclass.getDeclaredFields();
for(Field f : declaredFields){
System.out.println(f);
// getModifiers()获取访问修饰符 0:default 1:public 2:private
//System.out.println(f.getModifiers());
System.out.println(Modifier.toString(f.getModifiers()));
// getType() 获取数据类型
Class type = f.getType();
System.out.println(type.getName());
// getName() 获取变量名
String fName = f.getName();
System.out.println(fName);
System.out.println();
}
// getMethods() 获取自己类及所有父类中的public方法
Method[] methods = pclass.getMethods();
for (Method m : methods){
System.out.println(m);
}
// getDeclaredMethods() 获取自己类的所有方法
Method[] declaredMethods = pclass.getDeclaredMethods();
for(Method m : declaredMethods){
System.out.println(m);
//访问修饰符
int modifiers = m.getModifiers();
System.out.println("\t"+Modifier.toString(modifiers));
//返回值类型
System.out.println("\t"+m.getReturnType().getName());
//方法名
System.out.println("\t"+m.getName());
//参数
Class[] parameterTypes = m.getParameterTypes();
if(!(parameterTypes==null && parameterTypes.length==0)){
for( Class p : parameterTypes){
System.out.print("\t"+p.getName());
}
System.out.println();
}
//抛出异常
Class[] exceptionTypes = m.getExceptionTypes();
if(!(exceptionTypes == null && exceptionTypes.length==0)){
for(int i=0; i<exceptionTypes.length; i++){
System.out.print("\t"+exceptionTypes[i].getName());
}
System.out.println();
}
//注解
Annotation[] annotations = m.getAnnotations();
for(Annotation a :annotations){
System.out.println("\t"+a);
}
}
// getConstructors()当前运行时类中public的构造器
Constructor[] constructors = pclass.getConstructors();
for(Constructor c : constructors){
System.out.println(c);
}
// getDeclaredConstructors() 当前运行时类中所有声明的构造器
Constructor[] declaredConstructors = pclass.getDeclaredConstructors();
for(Constructor c : declaredConstructors){
System.out.println(c);
}
}
获取运行时类的父类及其泛型
@Test
public void test9(){
Class pclass = Person.class;
// getSuperclass() 获取父类
Class superclass = pclass.getSuperclass();
System.out.println(superclass);
// getGenericSuperclass() 带泛型的父类
Type genericSuperclass = pclass.getGenericSuperclass();
System.out.println(genericSuperclass);
// 获取父类泛型的实际类型
ParameterizedType type = (ParameterizedType) genericSuperclass;
Type[] actualTypeArguments = type.getActualTypeArguments();
System.out.println(actualTypeArguments[0]);
}
获取运行时类的接口
@Test
public void test10(){
Class pclass = Person.class;
// getInterfaces() 运行时类接口
Class[] interfaces = pclass.getInterfaces();
for(Class i : interfaces){
System.out.println(i);
}
//运行时类父类接口
Class[] interfaces1 = pclass.getSuperclass().getInterfaces();
for(Class i : interfaces1){
System.out.println(i);
}
}
获取运行时类所在包、注解
@Test
public void test11(){
Class pclass = Person.class;
// getPackage()
System.out.println(pclass.getPackage());
// getAnnotations
Annotation[] annotations = pclass.getAnnotations();
for(Annotation a : annotations){
System.out.println(a);
}
}
调用运行时类的指定结构
@Test
public void test12() throws Exception{
Class pclass = Person.class;
Person p = (Person) pclass.newInstance();
// getDeclaredConstructor(paramType...) 获取指定构造器
Constructor con1 = pclass.getDeclaredConstructor(String.class);
Constructor con2 = pclass.getDeclaredConstructor(String.class, int.class);
con1.setAccessible(true);
// newInstance(args...) 调用构造器进行实例化
Person p1 = (Person) con1.newInstance("jingyu");
Person p2 = (Person) con2.newInstance("wanfeng", 25);
System.out.println(p1.toString());
System.out.println(p2.toString());
// getField()获取指定属性 需要属性为public
Field age = pclass.getField("age");
//Field id = pclass.getField("id");
// getDeclaredField() 获取指定属性
Field name = pclass.getDeclaredField("name");
// field.set(Object, value) 设置指定属性
// setAccessiable(true) 若为默认或私有属性设置属性可访问
age.set(p, 23);
name.setAccessible(true);
name.set(p, "wanfeng");
// field.get(Object) 获取指定属性的值
Object age_value = age.get(p);
System.out.println(age_value);
System.out.println(name.get(p));
//getDeclaredMethod(name, param1Type, param2Type, ...) 获取指定方法
Method showAge = pclass.getDeclaredMethod("showAge");
Method showName = pclass.getDeclaredMethod("showName");
Method setName = pclass.getDeclaredMethod("setName", String.class);
// invoke(Object, args...)
// 调用指定方法,默认和私有方法需要设置可访问
// 返回值即为类中声明的返回值
showAge.invoke(p);
setName.invoke(p, "jingyu");
showName.setAccessible(true);
Integer res = (Integer) showName.invoke(p);
System.out.println(res);
// invoke(Class, args...) 调用静态方法
Method coding = pclass.getDeclaredMethod("coding", String.class);
coding.setAccessible(true);
String status = (String) coding.invoke(Person.class, "BUSY");
System.out.println(status);
}
十、IO流
IO流概述
Input/Output,用于处理设备间的数据传输,如读写文件,网络通讯。java.io包下提供了各种标准的类和接口用以获取不同种类的数据,并通过标准的方法进行输入输出。
流可以理解为二进制数据的传输
按数据单位分为
- 字节流:操作数据单位为8bit。使用InputStream/OutputStream。非文本数据常用字节流
- 字符流:操作数据单位为16bit。使用Reader/Writer。
按流的角色可分为
- 节点流:用于传输数据,直接作用在文件上
- 处理流:节点流的外层,用以辅助节点流。处理流的外层流也是处理流
IO流体系
流的基本使用(以字节流为例)
/**
* 字节流测试 效率较低,不建议使用
*/
@Test
public void test_FileStream(){
File file = new File(TEXT_FILE_PATH);
try {
//使用File对象创建输出流
OutputStream os = new FileOutputStream(file, true);
//写入的内容
String text = "既是终点,也是起点";
//将内容转换为字节并调用write方法写入
os.write(text.getBytes());
//释放输出流
os.close();
//使用File对象创建输入流
InputStream inputStream = new FileInputStream(file);
//一次性读取多少字节
byte[] bytes = new byte[1024];
//StringBuilder对象接受读取到的字节
StringBuilder stringBuilder = new StringBuilder();
//读取到的字节长度
int length = 0;
//循环读取
while((length = inputStream.read(bytes)) != -1){
stringBuilder.append(new String(bytes, 0, length));
}
//释放输入流
inputStream.close();
System.out.println(stringBuilder.toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
不同类型的流在读取和写入数据的步骤基本相同,只是在构造时有差别
File file = new File(TEXT_FILE_PATH);
//缓冲字节流的构造 需要字节输入流作为构造参数
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
BufferedInputStream bufferedInputStream = new BufferedInputStream(Files.newInputStream(file.toPath()));
//字符流的构造 需要字节输入流作为构造
OutputStreamWriter outputStream = new OutputStreamWriter(new FileOutputStream(file, true));
InputStreamReader inputStream = new InputStreamReader(new FileInputStream(file));
//字符流便捷类 使用File对象构造即可,与字符流相同
FileWriter fileWriter = new FileWriter(file, true);
FileReader fileReader = new FileReader(file);
//字符缓冲流
BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file, true));
例题
【1】开发中用new还是反射调用公共的结构?
通常用new的方式。在不确定实例化哪个类时可以用反射
【2】反射和封装性矛盾吗?如何解释
不矛盾。封装性是指将类中的私有结构隐藏起来,往往这些结构是不建议直接访问和调用的,但是如果有特殊需求的情况是可以通过反射直接访问私有结构的。个人理解是,反射是为了满足特殊情况而出现的,所以不矛盾