Java从入门到精通(二)

简介: Java从入门到精通(二)

Java从入门到精通(二)


二维数组的调用
System.out.println(arr1[0][1]);
System.out.println(arr2[1][1]);
arr3[1] =  new String[4];
System.out.println(arr3[1][0]);
二维数组的属性
System.out.println(arr4.length);
System.out.println(arr4[1].length);

遍历二维数组

for (int i = 0; i < arr6.length; i++) {
    for (int j = 0; j < arr6[i].length; j++) {
        System.out.print(arr6[i][j] + " ");
    }
    System.out.println();
}
数组元素的默认初始化值

规定:二维数组分为外层数组的元素,内层数组的元素

  • 针对于初始化方式一:比如:int[][] arr = new int[4][3];
外层元素的初始化值为:地址值
内层元素的初始化值为:与一维数组初始化情况相同
  • 针对于初始化方式二:比如:int[][] arr = new int[4][];
外层元素的初始化值为:null
内层元素的初始化值为:不能定义,否则报错
数组的内存解析

数组的常见算法

数组的创建与元素赋值

杨辉三角(二维数组)、回形数(二维数组)、6个数,1-30之间随机生成且不重复

针对于数值型的数组

最大值、最小值、总和、平均数等

数组的赋值与复制
int[] array1, array2;
array1 = new int[]{1, 2, 3, 4};
赋值
array1 = array2;

理解:将 array1 保存的数组的地址值赋给了 array2,使得 array1 和 array2 共同指向堆空间的同一个数组实体

复制
array2 = new int[array1.length];
for (int i = 0; i < array2.length; i++) {
    array2[i] = array1[i];
}

理解:通过 new 的方式,给 array2 在堆空间中新开辟了数组的空间,将 array1 数组中的元素值一个一个的赋值到 array2 数组中

数组元素的反转
// 方式一:
for (int i = 0; i < arr1.length / 2; i++) {
    String temp = arr1[i];
    arr1[i] = arr1[arr1.length - i - 1];
    arr1[arr1.length - i - 1] = temp;
}
// 方式二:
for(int i = 0, j = arr1.length - 1; i < j; i++, j--) {
    String temp = arr1[i];
    arr1[i] = arr1[j];
    arr1[j] = temp;
}

数组中指定元素的查找

线性查找

实现思路:通过遍历的方式,一个一个的数据进行比较、查找

适用性:具有普遍适用性

二分法查找

实现思路:每次比较中间值,折半的方式检索

适用性:(前提 :数组必须有序)

/**
* 折半查找法
* @param arr 有序的数组
* @param dest 要找的数字
*/
public void binarySearch(int[] arr, int dest) {
    // 初始首索引
    int head = 0;
    // 初始末索引
    int end = arr.length  - 1;
    boolean isFlag1 = true;
    while (head <= end) {
        int middle = (head + end) / 2;
        if (dest == arr[middle]) {
            System.out.println("找到了指定的元素,位置为:" + middle);
            isFlag1 = false;
            break;
        }else if (arr[middle] > dest) {
            end = middle - 1;
        }else {
            head = middle + 1;
        }
    }
    if (isFlag1) {
        System.err.println("很遗憾,没有找到");
    }
}

数组的排序算法

选择排序

直接选择排序

堆排序

交换排序

冒泡排序

快速排序

插入排序

直接插入排序

折半插入排序

希尔排序

归并排序

桶式排序

基数排序

理解:

衡量排序算法的优劣

时间复杂度

空间复杂度

稳定性

排序的分类

内部排序

外部排序(需要借助于磁盘)

不同排序算法的时间复杂度

image.png

手写冒泡排序

public void bubbleSort(int[] arr){
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

Arrays 工具类的使用

理解

定义在 java.util 包下

Arrays:提供了很多操作数组的方法

使用

package com.atguigu.java;
import java.util.Arrays;
/**
* Filename : ArraysTest.java
* Author : keke
* Creation time : 下午9:52:42 2021年11月2日
* Description :
* java.util.Arrays:操作数组的工具类,里面定义了很多操作数组的方法
*/
public class ArraysTest {
  public static void main(String[] args) {
    // 1.boolean equals(int[] a,int[] b):判断两个数组是否相等。
    int[] arr1 = {1, 2, 3, 4};
    int[] arr2 = {1, 3, 2, 4};
    System.out.println(Arrays.equals(arr1, arr2));
    // 2.String toString(int[] a):输出数组信息。
    System.out.println(Arrays.toString(arr1));
    // 3.void fill(int[] a,int val):将指定值填充到数组之中。
    Arrays.fill(arr1, 10);
    System.out.println(Arrays.toString(arr1));
    // 4.void sort(int[] a):对数组进行排序。
    Arrays.sort(arr2);
    System.out.println(Arrays.toString(arr2));
    // 5.int binarySearch(int[] a,int key):对排序后的数组进行二分法检索指定的值。
    int[] arr3 = {-98, -34, 2, 34, 54, 66, 79, 105, 210, 333};
    System.out.println(Arrays.binarySearch(arr3, 211));
  }
}

数组的常见异常

数组角标越界异常:ArrayIndexOutOfBoundsException

int[] arr = {1, 2, 3, 4, 5};
for (int i = 0; i <= arr.length; i++) {
    System.out.println(arr[i]);
}
System.out.println(arr[-2]);

空指针异常:NullPointerException

// 情况一:
int[] arr1 = {1, 2, 3};
arr1 = null;
System.out.println(arr1[1]);
// 情况2:
int[][] arr2 = new int[4][];
System.out.println(arr2[0][0]);
// 情况3:
String[] arr3 = new String[] {null, "BB", "CC"};
System.out.println(arr3[0].toString());

小知识:一旦程序出现异常,未处理时,就终止执行

面向对象

类与对象

面向对象学习的三条主线

Java 类及类的成员:属性、方法、构造器、代码块、内部类

面向对象的三大特征:封装、继承、多态、(抽象)

其它关键字:this、super、static、final、abstract、interface、package、import 等

“大处着眼,小处着手”

面向过程与面向对象

面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。

面向对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。

完成一个项目(或功能)的思路

根据问题需要,选择问题所针对的现实世界中的实体。

从实体中寻找解决问题相关的属性和功能,这些属性和功能就形成了概念世界中的类。

把抽象的实体用计算机语言进行描述,形成计算机世界中类的定义。即借助某种程序 语言,把类构造成计算机能够识别和处理的数据结构。

将类实例化成计算机世界中的对象。对象是计算机世界中解决问题的最终工具。

面向对象中两个重要的概念

类:对一类事物的描述,是抽象的、概念上的定义

对象:是实际存在的该类事物的每个个体,因而也称为实例(instance)。

面向对象程序设计的重点是类的设计

设计类,就是设计类的成员

二者的关系:对象,是由类 new 出来的,派生出来的

面向对象思想落地实现的规则一

创建类,设计类的成员

创建类的对象

通过“对象.属性”或“对象.方法”,调用对象的结构

补充:几个概念的使用说明

属性 = 成员变量 = field = 域 = 字段

行为 = 成员方法 = method = 方法 = 函数

创建类的对象 = 类的实例化 = 实例化类

对象的创建与对象的内存解析

典型代码:

Person p1 = new Person();
Person p2 = new Person();
Person p3 = p1; // 没有新创建一个对象,共用一个堆空间的对象实体

说明:

  • 如果创建了一个类的多个对象,则每个对象都拥有一套类的属性(非 static 的)
  • 意味着:如果修改一个对象的属性 a,则不影响另外一个属性 a 的值

内存解析

匿名对象

概念:创建的对象,没有显示地赋给一个变量名,即为匿名对象

特点:匿名对象只能调用一次

举例

new Phone().sendEmail();
new Phone().playGame();
new Phone().price = 1999;
new Phone().showPrice();

应用场景:

PhoneMall mall = new PhoneMall();
// 匿名对象的使用
mall.show(new Phone());
class PhoneMall{
    public void show(Phone phone) {
        phone.sendEmail();
        phone.playGame();
    }
}
理解“万事万物皆对象”
  1. 在 Java 语言范畴中,都将功能、结构等封装到类中,通过类的实例化,来调用类的具体功能结构
Scanner,String 等
文件:File
网络资源:URL

涉及到 Java 语言与前端 Html、后端数据库交互时,前后端的结构在 Java 层面交互时,都体现为类、对象

JVM 内存结构

编译完源程序以后,生成一个或多个字节码文件。使用 JVM 中的类的加载器和解释器对生成的字节码文件进行解释运行,意味着,需要将字节码文件对应的类加载到内存中,涉及到内存解析

虚拟机栈,即为平时提到的栈结构,将局部变量存储在栈结构中


堆,将 new 出来的结构加载在堆空间中,补充:对象是属性(非 static 的)加载在堆空间中


方法区:类的加载信息、常量池、静态域


类的结构之一:属性

类的设计中,两个重要的结构之一:属性


对比 属性 VS 局部变量


相同点


定义变量的格式:数据类型 变量名 = 变量值;

先声明,后使用

变量都有其对应的作用域

不同点:


在类中声明的位置的不同


属性:直接定义在类的一对{}内

局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量

关于权限修饰符的不同


属性:可以在声明属性时,指明其权限,使用权限修饰符


常用的权限修饰符:private、public、缺省、protected --> 封装性


目前,声明属性时,都使用缺省就可以了


局部变量:不可以使用权限修饰符的


默认初始化值的情况


属性:类的属性根据其类型,都有默认初始化值

整型(byte、short、int、long):0

浮点型(float、double):0.0

字符型(char):0(或 ‘\u0000’)

布尔型(boolean):false

引用数据类型(类、数组、接口):null

局部变量:没有默认初始化值,意味着:在调用局部变量之前,一定要显示赋值,特别地:线程在调用时,赋值即可

在内存中加载的位置


属性:加载到堆空间(非 static)

局部变量:加载到栈空间

补充:回顾变量的分类


方式一:按照数据类型

  • 方式二:按照在类中声明的位置

类的结构之二:方法

类的设计中,两个重要的结构之二:方法

  1. 方法的声明:
权限修饰符 返回值类型 方法名(形参列表){
    方法体
}

注意:static、final、abstract 来修饰的方法,后面再说

说明

权限修饰符:默认方法的权限修饰符先都使用 public

Java 规定的4种权限修饰符:private、protected、缺省、public --> 封装性再细说

返回值类型:有返回值 vs 没有返回值

如果方法有返回值,则必须在方法声明时,指定返回值的类型,同时,方法中,需要使用 return 关键字来返回指定类型的变量或常量:“return 数据;“

如果方法没有返回值,则方法声明时,使用 void 来表示,通常,没有返回值的方法中,就不能使用 return。但是,如果使用的话,只能 “return;” 表示结束此方法的意思

定义方法该不该有返回值?

题目要求

凭经验:具体问题具体分析

方法名:属于标识符,遵循标识符的命名规则和规范,“见名知义”

形参列表:方法可以声明零个,一个,或多个形参

格式:数据类型1 形参1, 数据类型2 形参2,…

定义方法时,该不该定义形参?

题目要求

凭经验:具体问题具体分析

方法体:方法功能的体现

方法的使用中,可以调用当前类的属性或方法

特殊的:方法 A 中又调用了方法 A 递归方法

方法中,不可以定义方法

return 关键字的使用

使用范围:使用在方法体中

作用

结束方法

针对于有返回值类型的方法,使用 “return 数据;” 方法返回所要的数据

注意点:return 关键字后面不可以声明执行语句

方法重载

概念

定义:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数 类型不同即可。

总结:“两同一不同”

同一个类,相同方法名
参数列表不同:参数个数不同,参数类型不同

举例

Arrays 类中重载的 sort() binarySearch(),PrintStream 中的 println()

// 以下4个方法能构成重载
public void getSum(int i, int j) {
    System.out.println(1);
}
public void getSum(double d1, double d2) {
    System.out.println(2);
}
public void getSum(String s, int i) {
    System.out.println(3);
}
public void getSum(int i, String s) {
    System.out.println(4);
}
// 以下3个方法不能构成重载
public int getSum(int i, int j){
    return 0;
}
public void getSum(int m, int n){
}
private void getSum(int i, int j){
}

判断是否构成方法的重载

严格按照定义判断:两同一不同

跟方法的权限修饰符、返回值类型、形参变量名、方法体都没关系

如何确定类中某一个方法的调用

方法名 --> 参数列表

面试题:方法的重载与重写的区别?

throws / throw
String / StringBuilder / StringBuffer
Collection / Collections
final / finally / finalize
sleep() / wait()
接口 / 抽象类
...

可变个数形参的方法

使用说明

jdk5.0 新增的内容

具体使用

可变个数形参格式:类型… 变量名

当调用可变个数形参时,传入的参数可以是0个,1个或多个

可变个数形参的方法与本类中方法名相同,形参不同的方法之间构成重载

可变个数形参的方法与本类中方法名相同,形参类型相同的数组之间不构成重载,换句话说,两者不能共存

可变个数形参在方法的形参中,必须声明在末尾

可变个数形参在方法的形参中,最多只能声明一个可变形参

举例说明

public class MethodArgsTest {
  public void show(int i) {
  }
  public void show(String s) {
    System.out.println("MethodArgsTest.show(String s)");
  }
  public void show(String... strs) {
    System.out.println("MethodArgsTest.show(String... strs)");
    for (int i = 0; i < strs.length; i++) {
      System.out.println(strs[i]);
    }
  }
    // 不能与上一个方法同时存在
  /*public void show(String[] strs) {
    System.out.println("MethodArgsTest.show(String[] strs)");
  }*/
  public void show(int i, String... strs) {
  }
}

调用时:

public static void main(String[] args) {
    MethodArgsTest test = new MethodArgsTest();
    test.show(1);
    test.show("Hello");
    test.show("Hello", "World");
    test.show();
    test.show(new String[] {"Hello", "World", "!"});
}

Java 的值传递机制

针对于方法内变量的赋值举例

public class ValueTransferTest {
  public static void main(String[] args) {
    int m = 10;
    int n = m;
    System.out.println("m = " + m + ", n = " + n);
    n = 20;
    System.out.println("m = " + m + ", n = " + n);
    Order o1 = new Order();
    o1.orderId = 1001;
    Order o2 = o1;
    System.out.println("o1.orderId = " + o1.orderId + ", o2.orderId = " + o2.orderId);
    // 赋值以后,o1 和 o2 的地址值相同,导致都指向堆空间的一个对象实体
    o2.orderId = 1002;
    System.out.println("o1.orderId = " + o1.orderId + ", o2.orderId = " + o2.orderId);
  }
}
class Order{
  int orderId;
}

规则:

如果变量的基本数据类型,此时赋值的是变量所保存的数据值

如果变量的引用数据类型,此时赋值的是变量所保存的数据的地址值

针对于方法的参数的概念

形参:方法定义时,声明的小括号内的参数

实参:方法调用时 ,实际传递给形参的数据

Java 中参数传递机制

规则:

如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值

如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值

推广:

如果变量的基本数据类型,此时赋值的是变量所保存的数据值

如果变量的引用数据类型,此时赋值的是变量所保存的数据的地址值
典型例题和内存解析

递归方法

定义

递归方法:一个方法体内调用它自身

理解

方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环。

举例

public class RecursionTest {
  // 例1:计算1-100之间所有自然数的和
  public int getSum(int n) {
    if (n == 1) {
      return 1;
    }
    return n + getSum(n - 1);
  }
  // 例2:计算1-100之间所有自然数的乘积
  public int getSum1(int n) {
    if (n == 1) {
      return 1;
    }
    return n * getSum1(n - 1);
  }
  /**
   * 例3:已知有一个数列:f(0) = 1, f(1) = 4, f(n+2) = 2 * f(n+1) + f(n),
   * 其中 n 是大于0的整数,求 f(10) 的值。
   */
  public int f(int n) {
    if (n == 0) {
      return 1;
    }else if (n == 1) {
      return 4;
    }
    return 2 * f(n - 1) + f(n - 2);
  }
  // 例4:斐波那契数列
  public int f1(int n) {
    if (n == 1 || n == 2) {
      return 1;
    }
    return f(n - 1) + f(n - 2);
  }
}

面向对象的特征之一:封装与隐藏

为什么引入

程序设计追求“高内聚,低耦合”。

高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;

低耦合 :仅对外暴露少量的方法用于使用。

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提 高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露 的暴露出来。这就是封装性的设计思想。

问题引入

当创建一个类的对象后,可以通过“对象.属性”的方式,对对象的属性进行赋值,这里,赋值操作要受到属性的数据类型和存储范围的制约,除此之外,没有其它制约条件,但是,在实际问题中,往往需要给属性赋值,加入额外的限制条件,这个条件就不能在属性声明时体现,只能通过方法进行限制条件的添加,这时需要避免用户再使用“对象.属性”的方式对属性进行赋值,则需要将属性声明为私有的 (private) --> 此时,针对属性就体现了封装性

封装性思想具体的代码体现

将类的属性私有化 (private),同时提供公共的 (public) 方法来获取 (getXxx) 和设置 (setXxx) 值

public class Circle{
    private double radius;
    public void setRadius(double radius){
        this.radius  = radius;
    }
    public double getRadius(){
        return radius;
    }
}

不对外暴露的私有方法

单例模式(将构造器私有化)

如果不希望类在包外被调用,可以将类设置为缺省的

Java 规定的四种权限修饰符

权限从小到大顺序

private < 缺省 < protected < public

具体的修饰范围

image.png

权限修饰符可用来修饰的结构说明

具体的,4种权限可以用来修饰类及类的内部结构:属性、方法、构造器、内部类

修饰类的话,只能使用:缺省 public

类的结构:构造器 Constructor

作用

创建对象

初始化对象的信息

使用说明

如果没有显示的定义类的构造器的话,则系统默认提供一个空参的构造器


定义构造器的格式:权限修饰符 类名(形参列表){}


一个类中定义的多个构造器,披此构成重载


一旦显示地定义了类的构造器之后,系统就不再提供默认的空参构造器


一个类中,至少会有一个构造器


举例

class Person{
  String name;
  int age;
  // 构造器
  public Person() {
    System.out.println("Person()..........");
  }
  public Person(String n) {
    name = n;
  }
  public Person(String n, int a) {
    name = n;
    age = a;
  }
  public void eat() {
    System.out.println("人吃饭");
  }
  public void study() {
    System.out.println("人可以学习");
  }
}

属性的赋值顺序

总结:属性赋值的先后顺序


默认初始化


显示初始化


构造器中初始化


通过“对象.方法”或“对象.属性”的方式赋值


以上操作的先后顺序:1 --> 2 --> 3 --> 4


JavaBean 的概念

所谓 JavaBean,是指符合如下标准的 Java 类:


类是公共的

有一个无参的公共的构造器

有属性,且有对应的 get、set 方法

this 关键字

可以调用的结构

属性、方法

构造器

调用属性、方法

this 理解为:当前对象或当前正在创建的对象


在类的方法中,可以使用“this.属性”或“this.方法”的方式,调用当前对象的属性或方法。但是,通常情况下,都选择省略"this.“,特殊情况下,如果方法的形参和类的属性同名时,必须显示地使用“this.变量”的方式,表明此变量是属性,而非形参


在类的构造器中,可以使用“this.属性”或“this.方法”的方式,调用当前正在创建的对象的属性或和方法。但是,通常情况下,都选择省略"this.“,特殊情况下,如果构造器的形参和类的属性同名时,必须显示地使用“this.变量”的方式,表明此变量是属性,而非形参


调用构造器

在类的构造器中,可以显示地使用“this(形参列表)”方式,调用本类中指定的其它的构造器


构造器中不能通过“this(形参列表)”方式调用自己


如果一个类中有 n 个构造器,则最多有 n - 1 个构造器中使用了“this(形参列表)”


规定:“this(形参列表)”必须声明在当前构造器的首行


构造器内部,最多只能声明一个“this(形参列表)”方式,用来调用其它构造器


关键字 package / import

package 关键字的使用

使用说明

为了更好的实现项目中类的管理,提供包的概念


使用 package 声明类或接口所属的包,声明在源文件的首行


包,属于标识符,遵循命名规则、规范,见名知义


每“."一次,就代表一层文件目录


举例

举例一:

举例二:MVC 设计模式

JDK 中的主要包介绍

java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和 Thread,提供常用功能

java.net----包含执行与网络相关的操作的类和接口。

java.io ----包含能提供多种输入/输出功能的类。

java.util----包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。

java.text----包含了一些java格式化相关的类

java.sql----包含了java进行JDBC数据库编程的相关类/接口

java.awt----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。 B/S C/S

import 关键字的使用

import:导入


在源文件中显示使用 import 结构导入指定包下的类和接口

声明在包的声明和类的声明之间

如果使用导入多个结构,则并列写出即可

可以使用“xxx.*”的方式,表示可以导入 xxx 包下的所有结构

如果使用的类或接口是 java.lang 包下定义的,则可以省略 import 结构

如果使用的类或接口是本包下定义的,则可以省略 import 结构

如果在源文件中,使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示

使用“xxx.*”的方式表明可以调用 xxx 包下的所有结构,但是如果的是 xxx 子包下的结构,则仍需要显示导入

import static:导入指定类或接口中的静态结构:属性或方法

面向对象的特征二:继承性

为什么要有继承性(继承性的好处)

减少了代码冗余,提高了代码复用性


便于功能的扩展


为之后的多态性的使用,提供了前提

继承性的格式
class A extends B{
}

A:子类、派生类、subClass

B:父类、超类、基类、superClass

子类继承父类之后有哪些不同

体现:一旦子类 A 继承父类 B 以后,子类 A 中就获取了父类 B 中声明的所有属性和方法,特别的,父类中声明为 private 的属性和方法,子类继承父类以后,仍然认为获取了父类中私有的结构,只有因为封装性的影响,使得子类不能直接调用父类的结构而已


子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的扩展子类和父类的关系,不同于子集和集合的关系extends:延展、扩展


Java 中继承性的说明

一个类可以被多个子类继承


Java 中类的单继承性:一个类只能有一个父类


子父类是相对的概念


子类直接继承的父类,称为直接父类,间接继承的父类,称为间接父类


子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法

java.lang.Object 类的理解

如果没有显示声明一个类的父类的话,则此类继承于 java.lang.Object

所有的 Java 类(除 java.lang.Object 类之外)都直接或间接地继承于 java.lang.Object

所有的 Java 类具有 java.lang.Object 类声明的功能

方法的重写

什么是方法的重写(override 或 overwrite)?

子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作

应用

重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法

举例

class Circle{
    /**
     * 求面积
     */
    public double findArea(){}
}
class Cylinder extends Circle{
    /**
     * 求表面积
     */
    public double findArea(){}
}
class Account{
    public boolean withdraw(double amt){}
}
class CheckAccount extends Account{
    public boolean withdraw(double amt){}
}
重写的规则

方法的声明:

权限修饰符 返回值类型 方法名 (形参列表) throws 异常的类型 {
    方法体
}

约定俗称:子类中的叫重写方法,父类中的叫被重写的方法


子类重写的方法的方法名与形参列表与父类被重写的方法的方法名与形参列表相同


子类重写的方法的权限修饰符不小于父类被重写的方法权限修饰符。特殊情况:子类不能重写父类中声明为 private 的方法


子类重写的方法的返回值类型


父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void


父类被重写的方法的返回值类型是 A,则子类重写的方法的返回值类型可以是 A 类及其子类


父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型


子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型


子类和父类中的同名同参数的方法要么都声明为非 static 的(考虑重写), 要么都声明为非 static 的(不是重写)


面试题

如何区分方法的重写与重载?


二者的概念


重载和重写的具体规则


重载不表现为多态性,重写表现为多态性


重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不 同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了 不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类 和子类的,即子类可以重载父类的同名不同参数的方法。 所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法, 这称为“早绑定”或“静态绑定”;


而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体 方法,这称为“晚绑定”或“动态绑定”。


引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”


super 关键字的使用

super 理解

父类的


super 调用的结构

属性

方法

构造器

super 调用属性和方法

可以在子类的方法或构造器中,通过使用“super.属性”或“super.方法”的方式,显示的调用父类中声明的属性或方法,但是,通常情况下,可以省略“super.”

特殊情况,当子类和父类中定义了同名的属性时,想在子类中定义父类中声明的属性,则必须显示地使用“super.属性”的方式,表明调用的是父类中声明的属性

特殊情况,当子类重写了父类的方法以后,想在子类的方法中定义父类中被重写的方法时,则必须显示地使用“super.方法”的方式,表明调用的是父类中被重写的方法

super 调用构造器

可以在子类的构造器中显示的使用“super(形参列表)”的方式,调用父类中声明的指定构造器

“super(形参列表)”的使用,必须声明在子类构造器的首行

在类的构造器中,针对于 “this(形参列表)”或“super(形参列表)”只能二选一,不能同时出

在构造器的首行,没有显示声明 “this(形参列表)”或“super(形参列表)”,则默认调用的是父类中的空参构造器

在类的多个构造器中,至少有一个构造器中使用了 “super(形参列表)”,调用父类中的构造器

子类对象实例化全过程

从结果上看:(继承性)

子类继承父类以后,就获取了父中声明的属性和方法


创建子类的对象,在堆空间中,就会加载所有父类中声明的属性


从过程上看

当通过子类的构造器创建子类对象时,一定会直接和间接地调用其父类的构造器,进而调用父类的父类的构造器,直到调用了 java.lang.Object 类中的空参构造器为止,正因为加载过所有的父类的结构,所以才可以看到内存中有父类指定结构,子类对象才可以考虑进行调用


强调说明

虽然创建子类对象时,调用了父类构造器,但是自始至终就创建过一个对象,即为 new 出来的子类对象

面向对象的特征之三:多态性

理解多态性

可以理解为一个事物的多种形态

何为多态性

对象的多态性:父类的引用指向子类的对象(或子类对象赋给父类引用)

举例:

Person p = new Man();
Object obj = new Date();

多态的使用:虚拟方法调用

有了对象的多态性以后,在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类重写父类的方法总结:编译看左边,运行看右边

多态性的使用前提

类的继承关系

方法的重写

多态性的应用举例

// 举例一:
public class AnimalTest {
  public static void main(String[] args) {
    AnimalTest test = new AnimalTest();
    test.func(new Dog());
    test.func(new Cat());
  }
  public void func(Animal animal) {
    animal.eat();
    animal.shout();
  }
}
class Animal{
  public void eat() {
    System.out.println("动物进食");
  }
  public void shout() {
    System.out.println("动物叫");
  }
}
class Dog extends Animal{
  @Override
  public void eat() {
    System.out.println("狗吃骨头");
  }
  @Override
  public void shout() {
    System.out.println("汪汪汪");
  }
}
class Cat extends Animal{
  @Override
  public void eat() {
    System.out.println("猫吃鱼");
  }
  @Override
  public void shout() {
    System.out.println("喵喵喵");
  }
}
//举例二:
class Order{
  public void method(Object obj) {
  }
}
// 举例三:
class Driver{
  public void doData(Connection conn) {
    // 规范的步骤去操作数据
  }
}

多态性使用的注意点

对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)

向上转型和向下转型

向上转型

多态

向下转型

为什么要使用向下转型:有了对象多态性以后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法,子类特有的属性和方法不能调用, 如何才能调用子类特有的属性和方法?使用向下转型

如何实现向下转型:使用强制类型转换符

注意点:

使用强转时,可能出现 ClassCastException 异常

为了避免在向下转型时出现 ClassCastException 的异常,在向下转型之前,先进行 instanceof 的判断,一旦返回 true,就进行向下转型,返回 false,不进行向下转型

instanceof 关键字的使用

a instanceof A 判断 a 是否是类 A 的实例,如果是,返回 true,如果不是,返回 false

如果 a instanceof A 返回 true,则 a instanceof B 返回 true,其中类 B 是类 A 的父类

a instanceof A 要求 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误


面试题

谈谈你对多态性的理解?

实现代码的通用性

Object 类中定义的 public boolean equals(Object obj){}

JDBC:使用 Java 程序操作(获取数据库连接、CRUD)数据库(MySQL、Oracle、DB2、SQL Server)

抽象类和接口的使用肯定体现了多态性(抽象类和接口不能实例化)

多态是编译时行为还是运行时行为?

运行时行为

证明:

package com.atguigu.test;
import java.util.Random;
//面试题:多态是编译时行为还是运行时行为?
//证明如下:
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();
  }
}

Object 类的使用

java.lang.Object 类的说明

Object 类是所有 Java 类的根父类

如果在类的声明中未使用 extends 关键字指明其父类,则默认父类为 java.lang.Object

Object 类中的功能(属性、方法)就具有通用性

属性:无

方法:equals() toString() getClass() hashCode() clone() finalize() wait() notify() notifyAll()

Object 类只声明了一个空参构造器

equals() 方法

使用

是一个方法,而非运算符

只能适用于引用数据类型

Object 类中 equals() 的定义

public boolean equals(Object obj) {
    return (this == obj);
}

说明:Object 类中定义的 equals() 方法和 == 的作用是相同的,比较两个地址值是否相同,即两个引用是否指向同一个对象实体

像 String、Date、File、包装类等都重写了 Object 类中的 equals() 方法,重写以后,比较的表示两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同

通常情况下,自定义的类如果使用 equals() 的话,也通常比较两个对象的“实体内容”是否相同,那么,就需要对 Object 的 equals() 进行重写,重写的规则:比较两个对象的实体内容是否相同

如何重写 equals()

手动重写举例:

public class Customer {
  String name;
  int age;
  /**
   * 重写的规则:比较两个对象的实体内容是否相同
   */
  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj instanceof Customer) {
      Customer cust = (Customer) obj;
      // 比较两个对象的属性是否都相同
      if (this.age == cust.getAge() && this.name.equals(cust.getName())) {
        return true;
      }
      return this.age == cust.getAge() && this.name.equals(cust.getName());
    }
    return false;
  }
}

开发中如何实现:自动生成的

public class Customer {
  String name;
  int age;
  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Customer other = (Customer) obj;
    return age == other.age && Objects.equals(name, other.name);
  }
}

回顾 == 运算符的使用

==:运算符

可以使用在基本数据类型变量和引用数据类型变量中

如果比较的是基本数据类型变量,比较的是两个变量保存的数据是否相等,不一定类型要相同

如果比较的是引用数据类型变量,比较的是两个地址值是否相同,即两个引用是否指向同一个对象实体

补充: == 符号使用时,必须保证符号左右两边的变量类型一致

toString() 方法

使用

当输出一个对象的引用时,实际上是调用当前对象的 toString()

Object 类中 toString() 的定义

public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

像 String、Date、File、包装类等都重写了 Object 类中的 toString() 方法,使得在调用对象的 toString() 方法时,返回对象“实体内容”信息


自定义类也可以重写 toString() 方法,当调用此方法时,返回对象的“实体内容”


如何重写 toString()

举例:

@Override
public String toString() {
    return "Customer [name=" + name + ", age=" + age + "]";
}

面试题

final、finally、finalize 的区别

== 和 equals() 的区别

单元测试方法

步骤

选中当前过程 - 右键选择:build path - add libraries - Junit 5 - 下一步

创建 Java 类,进行单元测试

此时的 Java 类要求

此类是 public 的

此类提供公共的无参构造器

此类中声明单元测试方法

此时的单元测试方法:方法的权限是 public ,没有返回值,没有形参

此单元测试方法上需要声明注解 @Test,并在单元测试类中导入:import org.junit.jupiter.api.Test;

声明号单元测试方法以后,就可以在方法体内写相关代码

写好代码以后,右键单击单元测试方法名,右键 run as - JUnit Test

说明:

如果执行结构没有任何异常,绿条

如果执行结构出现异常,红条

包装类的使用

为什么要有包装类(或封装器)

为了使基本数据类型的变量具有类的特征,引入包装类

基本数据类型与对应的包装类

image.png

需要掌握的类型间的转换

基本数据类型、包装类和 String 类型


简易版

基本数据类型 <—> 包装类:JDK 5.0 新特性:自动装箱与自动拆箱


基本数据类型、包装类 --> String:调用 String 重载的 valueOf(Xxx xxx)


String --> 基本数据类型、包装类:调用包装类的 parseXxx(String s)


注意:转换时,可能会报 NumberFormatException


应用场景举例

Vector 类中关于添加元素,只定义了形参为 Object 类型的方法

v.addElement(Object obj); // 基本数据类型 --> 包装类 --> 使用多态

static 关键字的使用

static:静态的

可以用来修饰

主要用来修饰类的内部结构

属性

方法

代码块

内部类

static 修饰属性

静态变量(类变量)

属性:按是否使用 static 修饰,又分为:静态属性 VS 非静态属性(实例变量)

实例变量:创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性,当修改其中一个对象中的非静态属性时,不会导致其它对象中同样的属性值的修改

静态变量:创建了类的多个对象,每个对象都共享同一个静态变量,当通过某一个对象修改静态变量时,会导致其它对象调用此静态变量时,是修改过了的

其它说明

静态变量随着类的加载而加载,可以通过“类.静态变量”的方式进行调用

静态变量的加载早于对象的创建

由于类只会加载一次,则静态变量在内存中也只会存在一份,存在方法区的静态域中

静态方法 非静态方法
yes no
对象 yes yes


  1. 静态属性举例:System.out, Math.PI
静态变量内存解析

static 修饰方法

静态方法、类方法

  1. 随着类的加载而加载,可以通过“类.静态方法”的方式进行调用
静态方法 非静态方法
yes no
对象 yes yes

静态方法中,只能调用静态的方法或属性

非静态方法中,即可以调用静态的方法或属性,也可以调用非静态的方法或属性

static 注意点

在静态方法中,不能使用 this、super 关键字

关于静态属性和静态方法的使用,都从生命周期的角度去理解

如何判定属性和方法应该使用 static 关键字

关于属性

属性是可以被多个对象所共享的,不会随着对象的不同而不同的

类中的常量也常常声明为 static 的

关于方法

操作静态属性的方法,通常设置为 static 的

工具类中的方法,习惯上声明为 static 的,比如:Arrays、Collections、Math

使用举例

举例一:Arrays、Collections、Math 等工具类

举例二:单例模式

举例三:

class Circle{

class Circle{
  private double radius;
  /**
   * 自动赋值
   */
  private int id;
  /**
   * 记录创建的圆的个数
   */
  private static int total;
  /**
   * static 声明的属性被所有对象共享
   */
  private static int init = 1001;
  public Circle() {
    id = init++;
    total++;
  }
  public Circle(double radius) {
    this();
    this.radius = radius;
  }
  public static int getTotal() {
    return total;
  }
  public double getRadius() {
    return radius;
  }
  public void setRadius(double radius) {
    this.radius = radius;
  }
  public int getId() {
    return id;
  }
  public double findArea() {
    return Math.PI * radius * radius;
  }
}

单例模式

设计模式的说明

理解

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。

常用设计模式

23种经典的设计模式 GoF

创建者模式

工厂方法模式

抽象工厂模式

单例模式

建造者模式

原型模式

结构型模式

适配器模式

装饰器模式

外观模式

代理模式

桥接模式

组合模式

享元模式

行为型模式

策略模式

模板方法模式

备忘录模式

观察者模式

迭代器模式

责任链模式

命令模式

状态模式

访问者模式

中介者模式

解释器模式

单例模式

要解决的问题

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例

具体代码的实现

饿汉式1:

class Bank{
  // 1.私有化类的构造器
  private Bank() {
  }
  // 2.内部创建类的对象
  // 4.要求此对象也必须声明为静态的
  private static Bank instance = new Bank();
  // 3.提供公共的静态方法,返回类的对象
  public static Bank getInstance() {
    return instance;
  }
}

饿汉式2:

class Bank{
  // 1.私有化类的构造器
  private Bank() {
  }
  // 2.内部创建类的对象
  // 4.要求此对象也必须声明为静态的
  private static Bank instance;
    static{
        instance = new Bank();
    }
  // 3.提供公共的静态方法,返回类的对象
  public static Bank getInstance() {
    return instance;
  }
}

懒汉式:

class Order{
  // 1.私有化类的构造器
  private Order() {
  }
  // 2.声明当前类对象,没有实例化
  // 4.此对象必须声明为 static 的
  private static Order instance = null;
  // 3.声明 public static 的返回当前类对象的方法
  public static Order getInstance() {
    if (instance == null) {
      instance = new Order();
    }
    return instance;
  }
}
两种方式的对比
  • 饿汉式
坏处:对象加载时间过长
好处:饿汉式是线程安全的
   懒汉式
   *        好处:延迟对象的创建
   *        目前的写法坏处:线程不安全 --> 到多线程内容时,再修改

main() 的使用说明

main() 方法作为程序的入口

main() 方法是一个普通的静态方法

main() 方法可以作为我们与控制台交互的方式(之前,使用 Scanner)

如何将控制台获取的数据传给形参:String[] args?

运行时:java 类名 “Tom” “123” “true” “Jerry”

sysout(args[0]); // "Tom"
sysout(args[2]); // "true"
sysout(args[4]); // 报异常

小结

public static void main(String[] args){
    // 方法体
}

权限修饰符:private 缺省 protected public —-> 封装性

修饰符:static final abstract native 可以用来修饰方法

返回值类型:无返回值 有返回值 --> return

方法名:需要满足标识符命名的规则、规范,见名知义

形参列表:重载 VS 重写;参数的值传递机制;体现对象的多态性

方法体:体现方法的功能

类的结构之四:代码块

又叫初始化块

代码块的作用

用来初始化类和对象的信息

分类

代码块如果有修饰的话,只能使用 static

静态代码块 VS 非静态代码块

静态代码块

内部可以有输出语句

随着类的加载而执行,而且只执行一次

作用:初始化类的信息

如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行

静态代码块的执行优先于非静态代码块的执行

静态代码块内只能调用静态的属性、静态的方法,不能调用非静态结构

非静态代码块

内部可以有输出语句

随着对象的创建而执行

每创建一个对象,就执行一次非静态代码块

作用:可以在创建对象时,对对象的属性进行初始化

如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行

非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法

实例化子类对象的加载顺序

涉及到父类、子类中静态代码块、非静态代码块构造器的加载顺序:由父及子,静态先行

LeafTest.java

package cn.tedu.java3;
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("尚硅谷");
    System.out.println("Leaf 的构造器");
  }
}
public class LeafTest{
  public static void main(String[] args){
    new Leaf(); 
    new Leaf();
  }
}

Son.java

package cn.tedu.java3;
class Father {
  static {
    System.out.println("11111111111");
  }
  {
    System.out.println("22222222222");
  }
  public Father() {
    System.out.println("33333333333");
  }
}
public class Son extends Father {
  static {
    System.out.println("44444444444");
  }
  {
    System.out.println("55555555555");
  }
  public Son() {
    System.out.println("66666666666");
  }
  public static void main(String[] args) { // 由父及子 静态先行
    System.out.println("77777777777");
    System.out.println("************************");
    new Son();
    System.out.println("************************");
    new Son();
    System.out.println("************************");
    new Father();
  }
}

属性的赋值顺序

默认初始化

显示初始化

对象初始化

有了对象以后,可以通过“对象.属性”或“对象.方法”的方式,进行赋值

代码块赋值

执行的先后顺序: 1 - 2 / 5 - 3 - 4

final 关键字的使用

可以用来修饰的结构

类、方法、变量

用法

final 用来修饰一个类:此类不能被其它类所继承,比如:String 类、System 类、StringBuffer 类

final 用来修饰方法:表明此方法不可以被重写,比如:Object 类的 getClass();

final 用来修饰变量:此时的“变量”就称为一个常量

final 修饰一个属性,可以考虑赋值的位置有:显示初始化、代码块中初始化、构造器中赋值

final 修饰局部变量尤其使用 final 修饰形参时,表明此形参是一个常量,当调用此方法时,给常量形参赋一个实参,一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值

static final 用来修饰属性,全局常量

abstract 关键字的使用

abstract:抽象的

可以用来修饰的结构

类、方法

具体的

abstract 修饰类:抽象类

此类不能实例化

抽象类一定有构造器,便于子类实例化时调用(涉及,子类对象实例化全过程)

开发中,都会提供抽象类的子类,完成相关的功能

abstract 修饰方法:

抽象方法抽象方法只有方法的声明,没有方法体

包含抽象方法的类,一定是一个抽象类,反之,抽象类中可以没有抽象方法

若子类重写了父类中的所有抽象方法后,此子类方可实例化

若子类没有重写了父类中的所有抽象方法后,此子类也是一个抽象类,需要使用 abstract 修饰

使用上的注意点

abstract 不能用来修饰属性、构造器等结构

abstract 不能用来修饰私有方法、静态方法、final 的方法、final 的类

abstract 的应用举例

举例一

public abstract class Vehicle{
    public abstract double calcFuelEfficiency(); // 计算燃料效率的抽象方法
    public abstract double calcTripDistance();  // 计算行驶距离的抽象方法
}
public class Truck extends Vehicle{
    public double calcFuelEfficiency(){ 
        //写出计算卡车的燃料效率的具体方法  
    }
    public double calcTripDistance(){  
        //写出计算卡车行驶距离的具体方法 
    }
}
public class RiverBarge extends Vehicle{
    public double calcFuelEfficiency() { 
        //写出计算驳船的燃料效率的具体方法 
    } 
    public double calcTripDistance()  {  
        //写出计算驳船行驶距离的具体方法
    }
}

举例二:

public class Circle extends GeometricObject{
  private double radius;
    @Override
  public double findArea() {
    return Math.PI * radius * radius;
  }
}
abstract class GeometricObject {
  public abstract double findArea();
}

举例三

IO 流中涉及到的抽象类:InputStream / OutputStream / Reader / Writer,在其内部定义了 read() / write()

模板方法设计模式

解决的问题

在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽 象出来,供不同子类实现。这就是一种模板模式。

举例

package cn.tedu.java;
public class TemplateTest {
  public static void main(String[] args) {
    Template template = new SubTemplate();
    template.spendTime();
  }
}
abstract class Template{
  /**
   * 计算某段代码所花费的时间
   */
  public void spendTime() {
    long start = System.currentTimeMillis();
    // 易变的部分
    code();
    long end = System.currentTimeMillis();
    System.out.println("花费的时间为:" + (end - start));
  }
  public abstract void code();
}
class SubTemplate extends Template{
  @Override
  public void code() {
    for(int i  = 2; i < 10000000; 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);
      }
    }
  } 
}

应用场景

数据库访问的封装

Junit 单元测试

JavaWeb 的 Servlet 中关于 doGet / doPost 方法调用

Hibernate 中模板程序

Spring 中 JDBCTemlate、HibernateTemplate 等

interface 关键字的使用

使用说明

接口使用 interface 定义

Java 中,接口和类是并列的两个结构

如何定义接口:定义接口是成员

JDK7及以前:只能定义全局常量和抽象方法

全局常量:public static final,但是书写时,可以省略不写

抽象方法:public abstract,但是书写时,可以省略不写

JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法和默认方法

接口中不能定义构造器,意味着接口不可以实例化

Java 开发中,接口通过类实现(implements)的方式来使用

如果实现类覆盖了接口中所有的抽象方法,则此实现类就可以实例化

如果实现类没有覆盖接口中所有的抽象方法,则此实现类仍为一个抽象类

Java 类可以实现多个接口 --> 弥补了 Java 单继承的局限性

格式:class AA extends BB implements CC, DD, EE {}

接口与接口之间可以继承,而且可以多继承

接口的具体使用,体现多态性

接口实际上可以看做是一种规范

举例

package cn.tedu.java1;
public class USBTest {
  public static void main(String[] args) {
    Computer com = new Computer();
    // 1.创建了接口的非匿名实现的非匿名对象
    Flash flash = new Flash();
    com.transforData(flash);
    // 2.创建了接口的非匿名实现的匿名对象
    com.transforData(new Printer());
    // 3.创建了接口的匿名实现的非匿名对象
    USB phone = new USB() {
      @Override
      public void stop() {
        System.out.println("手机结束工作");
      }
      @Override
      public void start() {
        System.out.println("手机开始工作");
      }
    };
    com.transforData(phone);
    // 4.创建了接口的匿名实现的匿名对象
    com.transforData(new USB() {
      @Override
      public void stop() {
        System.out.println("mp3 结束工作");
      }
      @Override
      public void start() {
        System.out.println("mp3 开始工作");
      }
    });
  }
}
class Computer{
  public void transforData(USB usb) {
    usb.start();
    System.out.println("具体传输数据的细节");
    usb.stop();
  }
}
interface USB{
  void start();
  void stop();
}
class Flash implements USB{
  @Override
  public void start() {
    System.out.println("U盘开启工作");
  }
  @Override
  public void stop() {
    System.out.println("U盘结束工作");
  }
}
class Printer implements USB{
  @Override
  public void start() {
    System.out.println("打印机开启工作");
  }
  @Override
  public void stop() {
    System.out.println("打印机结束工作");
  }
}

体会

  • 接口使用上满足多态性
  • 接口实际上定义了一种规范
  • 开发中,体会面向接口编程
体会面向接口编程的思想

面向接口编程,在应用程序中,调用的结构都是 JDBC 中定义的接口,不会出现具体某一个数据库厂商的 API

JDK8 中关于接口的新规范

接口中定义的静态方法,只能通过接口来调用

通过实现类对象,可以调用接口中的默认方法如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法

如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法 --> 类优先原则

如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错 --> 接口冲突,这就需要我们必须在实现类中重写此方法

如何在子类(或实现类)的方法中调用父类、接口中被重写的方法

public void myMethod() {
    method3(); // 调用自己定义的重写的方法
    super.method3(); // 调用父类中声明的
    // 调用接口中的默认方法
    CompareA.super.method3();
    CompareB.super.method3();
}

面试题

抽象类和接口有哪些异同?

相同点

不能实例化

都可以包含抽象方法

不同点

把抽象类和接口的定义、内部结构解释说明

类:单继承

接口:多继承

类与接口:多实现

代理模式

解决的问题

代理模式是 Java 开发中使用较多的一种设计模式。代理设计就是为其 他对象提供一种代理以控制对这个对象的访问。

举例

public class NetWorkTest {
  public static void main(String[] args) {
    Server server = new Server();
    ProxyServer proxyServer = new ProxyServer(server);
    proxyServer.browse();
  }
}
interface NetWork{
  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;
  }
  @Override
  public void browse() {
    check();
    work.browse();
  }
  public void check() {
    System.out.println("联网之前的检查工作");
  }
}

应用场景

应用场景:

安全代理:屏蔽对真实角色的直接访问。

远程代理:通过代理类处理远程方法调用(RMI)

延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象

比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理 模式,当需要查看图片时,用 proxy 来进行大图片的打开。

分类

静态代理(静态定义代理类)

动态代理(动态生成代理类)

JDK 自带的动态代理,需要反射等知识

工厂的设计模式

解决的问题

实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。

具体模式

**简单工厂模式:**用来生产同一等级结构中的任意产品。(对于增加新的产品, 需要修改已有代码)

**工厂方法模式:**用来生产同一等级结构中的固定产品。(支持增加任意产品)

**抽象工厂模式:**用来生产不同产品族的全部产品。(对于增加新的产品,无 能为力;支持增加产品族)

类的结构之五:内部类

定义

Java 中允许将一个类 A 声明在另一个类 B 中,则类 A 就是内部类,类 B 就是外部类

分类

成员内部类(静态、非静态)

局部内部类(方法内、代码块内、构造器内)

成员内部类的理解

一方面,作为外部类的成员

调用外部类的结构

可以被 static 修饰

可以被4种不同的权限修饰

另一方面,作为一个类

类内可以定义属性、方法、构造器等

可以被 final 修饰,表明此类不能被继承,言外之意,不使用 final,就可以被继承

可以被 abstract 修饰

局部内部类

如何实例化成员内部类的对象

public static void main(String[] args) {
    // 创建 Dog 实例(静态成员内部类)
    Dog dog = new Person.Dog();
    // 创建 Bird 实例(非静态成员内部类)
    Person p = new Person();
    Bird bird = p.new Bird();
}

如何在成员内部类中区分调用外部类的结构

class Person{
  String name = "小明";
    public void eat(){
    }
  // 非静态成员内部类
  class Bird{
    String name = "杜鹃";
    public void display(String name) {
      System.out.println(name); // 方法形参
      System.out.println(this.name); // 内部类属性
      System.out.println(Person.this.name); // 外部类属性
            // Person.this.eat();
    }
  }
}

局部内部类的使用

public class InnerClassTest1 {
  // 开发中很少见
  public void method() {
    class AA{
    }
  }
  /**
   * 返回一个实现了 Comparable 接口的类对象
   */
  public Comparable getComparable() {
    // 创建一个实现了 Comparable 接口的类:局部内部类
    /*class MyComparable implements Comparable{
      @Override
      public int compareTo(Object o) {
        // TODO Auto-generated method stub
        return 0;
      }
    }
    return new MyComparable();*/
    return new Comparable() {
      @Override
      public int compareTo(Object o) {
        // TODO Auto-generated method stub
        return 0;
      }
    };
  }
}

注意点:

在局部内部类的方法中,如果调用局部内部类所声明的方法中的局部变量话,要求此局部变量声明为 final 的

JDK7 及之前版本:要求此局部变量显示的声明为 final 的

JDK8 及之后版本:可以省略 final 的声明

public class InnerClassTest {
  public void method() {
    // 局部变量
    int num = 10;
    class AA{
      public void show() {
        // num = 10;
        System.out.println(num);
      }
    }
  }
}

总结

成员内部类和局部内部类,在编译以后,都会生成字节码文件

格式

成员内部类:外部类 $ 内部类名 .class

局部内部类:外部类 $ 数字 内部类名 .class

Debug 调试

image.png

异常

异常

异常的体系结构

java.lang.Throwable
         |--- java.lang.Error:一般不编写针对性的代码进行处理
         |--- java.lang.Exception:可以进行异常的处理
                  |--- 编译时异常(checked)
                           |--- IOException
                                  |--- FileNotFoundException
                           |--- ClassNotFoundException
                  |--- 运行时异常(unchecked)
                         |--- NullPointerException
                           |--- ArrayIndexOutOfBoundsException
                           |--- ClassCastException
                           |--- NumberFormatException
                           |--- InputMismatchException
                           |--- ArithmeticException

从程序的执行过程,看编译时异常和运行时异常

编译时异常:执行 javac.exe 命令时,可能出现的异常

运行时异常:执行 java.exe 命令时,出现的异常

常见的异常类型,请举例说明
public class ExceptionTest {
  // NullPointerException
  @Test
  public void test1() {
    int[] arr = null;
    System.out.println(arr);
    String str = null;
    System.out.println(str.charAt(0));
  }
  // IndexOutOfBoundsException
  @Test
  public void test2() {
    // ArrayIndexOutOfBoundsException
    int[] a = new int[10];
    System.out.println(a[10]);
    // StringIndexOutOfBoundsException
    String str = "abc";
    System.out.println(str.charAt(3));
  }
  // ClassCastException
  @Test
  public void test3() {
    Object obj = new Date();
    String str = (String) obj;
  }
  // NumberFormatException
  @Test
  public void test4() {
    String str = "abc";
    int parseInt = Integer.parseInt(str);
  }
  // InputMismatchException
  @Test
  public void test5() {
    int score = new Scanner(System.in).nextInt();
    System.out.println(score);
  }
  // ArithmeticException
  @Test
  public void test6(){
    int a = 1 / 0;
  }
  //
//  @Test
//  public void test7() {
//    File file = new File("hello.txt");
//    FileInputStream fileInputStream = new FileInputStream(file);
//    int read  = fileInputStream.read();
//    while (read != -1) {
//      System.out.print((char)read);
//        read = fileInputStream.read();
//    }
//    fileInputStream.close();
//  }
}

异常的处理

Java 异常处理的抓抛模型

“抛”,程序在正常执行过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象,并将此对象抛出,一旦抛出异常以后,其后的代码就不再执行

关于异常对象的产生:

系统自动生成的异常对象

手动生成一个异常对象,并抛出(throw)

“抓”,可以理解为异常的处理方式:

try-catch-finally

throws

异常处理方式一:try-catch-finally

try{
    // 可能出现异常的代码
} catch(异常类型1 变量名1){
    // 处理异常的方式1
} catch(异常类型2 变量名2){
    // 处理异常的方式2
} catch(异常类型3 变量名3){
    // 处理异常的方式3
}
...
finally{
   // 一定会执行的代码
}

使用说明

finally 是可选的

使用 try 将可能出现的异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去 cache 中进行匹配

一旦 try 中的异常对象匹配到某一个 catch 时,就进入 catch 中进行异常处理,一旦处理完成,就跳出当前的 try-catch 结构(在没有写 finally 的情况),继续执行其后的代码

catch 中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下无所谓

catch 中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面,否则报错

常用的异常对象处理的方式

String getMessage()

printStackTrace()

在 try 结构中声明的变量,出了 try 结构以后,就不能被调用

try-catc-finally 结构可以相互嵌套

总结:如何看待代码中的编译时异常和运行时异常?

使用 try-catch-finally 处理编译时异常,是将程序在编译时就不再报错,但是运行时仍可能报错,相当于使用 try-catch-finally 将一个编译时可能出现的异常,延迟到运行时出现

开发中由于运行时异常比较常见,所以通常就不针对运行时异常编写 try-catch-finally,针对于编译时异常,一定要考虑异常的处理

finally 的再说明

finally 是可选的

finally 中声明的是一定会被执行的代码,即使 catch中又出现异常了,try 中有 return 语句,catch 中有 return 语句等情况

像数据库连接,输入输出流、网络编程 Socket 等资源,JVM 是不能自动回收,需要自己手动的进行资源的释放,此时的资源释放,就需要声明在 finally 中

面试题

final、finally、finalize 的区别

类似:

throw 和 throws

Collection 和 Collections

String、StringBuilder 和 StringBuffer

ArrayList 和 LinkedList

HashMap 和 LinkedHashMap

重写和重载

结构不相似:

抽象类和接口

== 和 equals()

sleep() 和 wait()

异常处理方式之二:throws

"throws + 异常类型"写在方法声明出,指明此方法执行时,可能会抛出的异常类型,一旦当方法体执行时,出现异常,仍然会在异常代码处生成一个异常类的对象。此对象满足 throws 后异常类型时,就会被抛出,异常代码后续的代码,就不再执行

对比两种处理方式

try-catch-finally:真正的将异常给异常处理掉了

throws 的方式注释将异常抛给了方法的调用者,并没有真正将异常处理掉

体会开发中应该如何选择两种处理方式

如果父类中被重写的方法没有 throws 方式处理异常,则子类重写的方法也不能使用 throws,意味着如果子类重写的方法有异常,必须使用 try-catch-finally 方式处理

执行的方法中先后又调用了另外的几个方法,这几个方法是递进关系执行的,建议这几个方法使用 throws的方式进行处理,而执行方法 a 可以考虑使用 try-catch-finally 方式进行处理

补充:

方法重写的规则之一:子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型

手动抛出异常对象

使用说明

在程序执行中,除了自动抛出异常对象的情况之外,还可以手动 throw 一个异常类的对象

面试题

throw 和 throws 的区别

throw:表示抛出一个异常类的对象,生成异常对象的过程,声明在方法体内

throws:属于异常处理的一种方式,声明在方法的声明处

典型例题

class Student{
  private int id;
  public void regist(int id)  {
    if (id > 0) {
      this.id = id;
    }else {
      // System.out.println("您输入的数据非法");
      // 手动抛出异常对象
      // throw new RuntimeException("输入的数据非法");
      // throw new Exception("输入的数据非法");
      throw new MyException("不能输入负数");
    }
  }
  @Override
  public String toString() {
    return "Student [id=" + id + "]";
  }
}

自定义异常类

如何自定义一个异常类

继承于现有的异常结构:RuntimeException Exception

提供全局常量 serialVersionUID

提供重载的构造器

public class MyException extends RuntimeException{
  static final long serialVersionUID = 12345678921234L;
  public MyException() {
  }
  public MyException(String msg) {
    super(msg);
  }
}

多线程

程序、进程和线程的理解

程序(program)

概念

是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码。

进程(process)

概念

是程序的一次执行过程,或是正在运行的一个程序。

说明

进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

线程(thread)

概念

进程可进一步细化为线程,是一个程序内部的一条执行路径。

说明

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

内存结构

进程可以细化为多个线程

每个线程拥有自己独立的:栈、程序计数器

多个线程共享同一个进程中的结构:方法区、堆

并行与并发

单核 CPU 与 多核 CPU 的理解

单核 CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程 的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费 才能通过,那么 CPU 就好比收费人员。如果有某个人不想交钱,那么收费人员可以 把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为 CPU 时间单元特别短,因此感觉不出来。

如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)

一个Java应用程序java.exe,其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。

并行与并发的理解

**并行:**多个CPU同时执行多个任务。比如:多个人同时做不同的事。

**并发:**一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

创建多线程的两种方式

方式一:继承 Thread 类的方式

创建一个继承于 Thread 类的子类

重写 Thread 类的 run() --> 将此线程声明在 run() 中

创建继承于 Thread 类的子类的对象

通过此对象调用 start():

启动当前线程

调用当前线程的 run()

说明两个问题:

启动一个线程,必须调用 start(),不能调用 run() 的方式启动线程

如果再启动一个线程,必须重新创建一个 Thread 子类的对象,调用此对象的 start()

方式二:实现 Runnable 接口的方式

创建一个实现了 Runnable 接口的类

实现类去实现 Runnable 中的抽象方法:run()

创建实现类的对象

将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象

通过 Thread 类的对象调用 start()

两种方式的对比

开发中,优先选择实现 Runnable 接口的方式

原因:

实现的方式没有类的单继承性的局限性

实现的方式更适合来处理多个线程有共享数据的情况

联系:public class Tread implements Runnable

相同点:

两种方式都需要重写 run(),将线程要执行的逻辑声明在 run() 中

目前两种方式,要想启动线程,都是调用的 Thread 类中的 run()

Thread 类中的常用方法

常用方法

start():启动当前线程,调用当前线程的 run()

run():通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中

currentThread():静态方法,返回执行当前代码的线程

getName():获取当前线程的名字

setName():设置当前线程的名字

yeild():释放当前 CPU 的执行权

join():在线程 a 中调用线程 b 的 join(),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完以后,线程 a 才结束阻塞状态

stop():已过时,当执行此方法时,强制结束当前线程

sleep(long millis):让当前线程“睡眠”指定的 millis 毫秒,在指定的 millis 毫秒时间内,当前线程是阻塞状态

isAlive():判断当前线程是否存活

线程的优先级

MAX_PRIORITY:10

MIN _PRIORITY:1

NORM_PRIORITY:5 --> 默认优先级

如何获取和设置当前线程的优先级

getPriority():获取线程的优先级

setPriority(int newPriority):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程 CPU 的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行

线程的通信:wait()、notify()、 notifyAll():此三个方法定义在 Object 类中的

线程的分类

守护线程

用户线程

Thread 的生命周期

说明:

生命周期关注两个概念:状态,相应的方法

关注

状态 a --> 状态 b:哪些方法执行了(回调方法)

某个方法主动调用:状态 a --> 状态 b

阻塞:临时状态,不可以作为最终状态

死亡:最终状态

线程的同步机制

背景

创建三个窗口买票,总票数为100张,使用实现 Runnable 接口的方式

问题,买票过程中,出现了重票、错票 --> 出现了线程安全的问题

出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其它线程参与进来,也操作车票

如何解决:当一个线程 a 在操作 ticket 的时候,其它线程不能参与进来,直到线程 a 操作完 ticket 时,其它线程才可以开始操作 ticket,这种情况即使线程 a 出现了阻塞,也不能改变

Java 解决方案:同步机制

在 Java 中,通过同步机制,来解决线程的安全问题

方式一:同步代码块

synchronized(同步监视器){
  // 需要被同步的代码
}

说明:

操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了

共享数据:多个线程共同操作的变量,比如:ticket 就是共享数据

同步监视器,俗称:锁。任何一个类的对象,都可以充当锁

要求:多个线程必须要公用同一把锁

**补充 **

在实现 Runnable 接口创建多线程的方式中,可以考虑 this 充当同步监视器

继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,不妨将此方法声明为同步的

同步方法仍然涉及到同步监视器,只是不需要显示声明

非静态同步方法的同步监视器是:this

静态同步方法的同步监视器是:当前类本身

方式三:Lock 锁 — JDK 5.0新增

面试题:synchronized 与 Lock 的异同

同:二者都可以解决线程安全问题

异:

synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器

Lock 需要手动的启动同步(lock()),同时,结束同步也要手动的实现(unLock())

优先使用顺序:Lock –> 同步代码块(已经进入了方法体,分配了相应资源) –> 同步方法(在方法体之外)

利弊

好处:同步的方式,解决了线程的安全问题

局限性:操作同步代码时,只能有一个线程参与,其它线程等待,相当于是一个单线程的过程,效率低

面试题

Java 是如何解决线程安全问题的,有几种方式?并对比几种方式的不同?

synchronized 和 Lock 方式解决线程安全问题的对比

线程安全的单例模式(懒汉式)

class Bank{
    private static Bank instance = null;
    private Bank(){
    }
    public static Bank getInstance() {
        // 方式一:效率稍差
        /*synchronized (Bank.class) {
            if (instance == null){
                instance = new Bank();
            }
            return instance;
        }*/
        // 方式二:效率更高
        if (instance == null){
            synchronized (Bank.class) {
                if (instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

面试题:写一个线程安全的单例模式

饿汉式

懒汉式

死锁问题

死锁的理解

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

说明

出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

使用同步时,要避免出现死锁

public class ThreadTest {
    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 = " + s1);
                        System.out.println("s2 = " + s2);
                    }
                }
            }
        }.start();
        new Thread(() -> {
            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 = " + s1);
                    System.out.println("s2 = " + s2);
                }
            }
        }).start();
    }
}

线程通信

涉及到的三个方法

wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器

notify():一旦执行此方法,就会唤醒被 wait 的一个线程,如果有多个线程被 wait,就唤醒优先级高的那个

notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程

说明

wait() notify() notifyAll() 三个方法必须使用在同步代码块或同步方法中

wait() notify() notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException 异常

wait() notify() notifyAll() 三个方法是定义在 java.lang.Object 类中

面试题

sleep() 和 wait() 的异同

相同点:

一旦执行方法,都可以使得当前线程进入阻塞状态

不同点:

两个方法声明的位置不同:Thread 类中声明 sleep(),Object 类中声明 wait()

调用的要求不同:sleep() 可以在任何需要的场景下调用,wait() 必须使用在同步代码块或同步方法中

关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁

小结

会释放锁的操作

当前线程的同步方法、同步代码块执行结束。

当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行。

当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束。

当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。

不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行

线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)。

应尽量避免使用 suspend() 和 resume() 来控制线程

JDK 5.0新增线程创建的方式

方式一

创建线程的方式三:实现 Callable 接口 — JDK 5.0新增

步骤

创建一个实现实现 Callable 的实现类

实现 call 方法,将此线程需要执行的操作声明在 call() 中

创建 Callable 接口的实现类

将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的实现类

将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()

获取 Callable 中 call() 方法的返回值

public class ThreadNew {
    public static void main(String[] args) {
        NumTread numTread = new NumTread();
        FutureTask future = new FutureTask(numTread);
        new Thread(future).start();
        try {
            // get() 返回值即为 FutureTask 构造器形参 Callable 实现类重写的 call() 的返回值
            Object sum = future.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class NumTread implements Callable{
    @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;
    }
}

说明:

如何理解实现 Callable 接口创建多线程方式比实现 Runnable 接口创建多线程方式强大

call() 可以有返回值

call() 可以抛出异常,被外面的操作捕获,获取异常信息

Callable 是支持泛型的

方式二

创建线程的方式四:使用线程池

步骤

提供指定线程数量的线程池

执行指定的线程的操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象

关闭连接池

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) service;
        // 设置线程池属性
        // System.out.println(service.getClass());
        // executor.setCorePoolSize(15);
        // executor.setKeepAliveTime();
        // 适合使用于 Runnable
        service.execute(new NumberThread());
        service.execute(new NumberThread1());
        // service.submit(); 适合使用于 Callable
        service.shutdown();
    }
}
class NumberThread 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 NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

好处

提高响应速度(减少了创建新线程的时间)

降低资源消耗(重复利用线程池中线程,不需要每次都创建)

便于线程管理

corePoolSize:核心池的大小

maximumPoolSize:最大线程数

keepAliveTime:线程没有任务时最多保持多长时间后会终止

面试题:Java 中多线程的创建有几种方式?四种

常用类

String 类的使用

概述

String:字符串,使用一对""引起来表示

String 声明为 final 的,不可被继承

String 实现了 Serializable 接口:表示字符串是支持序列化的

String 实现了 Comparable 接口:表示 String 可以比较大小

String 内部定义了 final char[] value 用于存储字符串数据

通过字面量的方式给一个字符串赋值,此时的字符串值声明在字符串常量池中

字符串常量池中是不会存储相同内容(使用 String 类的 equals() 比较,返回 true)的字符串的

String的不可变性

说明

当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的 value 进行赋值

当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值

当调用 String 的 replace() 方法修改指定的字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值

代码举例

@Test
public void test1(){
    // 字面量的定义方式
    String s1 = "abc";
    String s2 = "abc";
    s1 = "hello";
    // 比较 s1 和 s2 的地址值
    System.out.println("s1 == s2 = " + (s1 == s2));
    System.out.println("s1 = " + s1);
    System.out.println("s2 = " + s2);
    String s3 = "abc";
    s3 += "def";
    System.out.println("s3 = " + s3);
    System.out.println("s2 = " + s2);
    String s4 = "abc";
    String s5 = s4.replace('a', 'm');
    System.out.println("s4 = " + s4);
    System.out.println("s5 = " + s5);
}
图示

String 实例化的不同方式
方式说明
  • 方式一:通过字面量定义的方式
  • 方式二:通过构造器的方式
代码举例
@Test
public void test2(){
    // 声明在方法区中的字符串常量池中
    String s1 = "JavaEE";
    String s2 = "JavaEE";
    // 保存的地址值是数据在堆空间中开辟以后对应的地址值
    String s3 = new String("JavaEE");
    String s4 = new String("JavaEE");
    System.out.println(s1 == s2); // true
    System.out.println(s1 == s3); // false
    System.out.println(s1 == s4); // false
    System.out.println(s3 == s4); // false
}
面试题
  • String s = new String(“abc”); 方式创建对象,在内存中创建了几个对象
    两个:一个是堆空间中 new 结构,另一个是 char[] 对应的常量池中的数据:“abc”
图示

字符串拼接方式赋值的对比

说明

常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。

只要其中有一个是变量,结果就在堆中

如果拼接的结果调用 intern() 方法,返回值就在常量池中

代码举例

@Test
public void test3(){
    String s1 = "javaEE";
    String s2 = "hadoop";
    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;
    System.out.println(s3 == s4); // true
    System.out.println(s3 == s5); // false
    System.out.println(s3 == s6); // false
    System.out.println(s3 == s7); // false
    System.out.println(s5 == s6); // false
    System.out.println(s5 == s7); // false
    System.out.println(s6 == s7); // false
    // 返回值得到的 s8 使用的常量值中已经存在的"javaEEhadoop"
    String s8 = s5.intern();
    System.out.println(s8 == s4); // true
}
@Test
public void test4(){
    String s1 = "javaEEhadoop";
    String s2 = "javaEE";
    String s3 = s2 + "hadoop";
    System.out.println(s1 == s3);
    final String s4 = "javaEE";
    String s5 = s4 + "hadoop";
    System.out.println(s1 == s5);
}

常用方法

int length():返回字符串的长度: return value.length

char charAt(int index): 返回某索引处的字符 return value[index]

boolean isEmpty():判断是否是空字符串:return value.length == 0

String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写

String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写

String trim():返回字符串的副本,忽略前导空白和尾部空白

boolean equals(Object obj):比较字符串的内容是否相同

boolean equalsIgnoreCase(String anotherString):与 equals 方法类似,忽略大小写

String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”

int compareTo(String anotherString):比较两个字符串的大小

String substring(int beginIndex): 返回一个新的字符串, 它是此字符串的从 beginIndex 开始截取到最后的一个子字符串。

String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从 beginIndex 开始截取到 endIndex (不包含)的一个子字符串。

boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束

boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始

boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始

boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true

int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引

int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始

int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引

int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索

indexOf 和 lastIndexOf 方法如果未找到都是返回-1

String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

String replace(CharSequence target, CharSequence replacement):使 用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。

String replaceAll(String regex, String replacement) :使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

String replaceFirst(String regex, String replacement) :使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。

String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。

String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个,如果超过了,剩下的全部都放到最后一个元素中。

String 与其它结构的转换

与基本数据类型、包装类之间的转换

String --> 基本数据类型、包装类:调用包装类的静态方法:parseXxx(str)

基本数据类型、包装类 --> String:调用 String 重载的 valueOf(xxx)

@Test
public void test1(){
    String str1 = "123";
    int num = Integer.parseInt(str1);
    String str2 = String.valueOf(num);
    String str3 = num + "";
    System.out.println(str1 == str3);
}

与 char[] 之间的转换

String --> char[]:调用 String 的 toCharArray()

char[] --> String:调用 String 的 构造器

@Test
public void test2(){
    String str1 = "abc123";
    char[] charArray = str1.toCharArray();
    for (int i = 0; i < charArray.length; i++) {
        System.out.println(charArray[i]);
    }
    char[] arr = new char[]{'h', 'e', 'l', 'l', 'o'};
    String str2 = new String(arr);
    System.out.println(str2);
}

与 byte[] 之间的转换

编码:String --> byte[]:调用 String 的 getBytes()

解码:byte[] --> String:调用 String 的构造器

编码:字符串 --> 字节

解码:编码的逆过程 字节 --> 字符串

说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码

@Test
public void test3() throws UnsupportedEncodingException {
    String str1 = "abc123中国";
    // 使用默认的字符集进行转换
    byte[] bytes = str1.getBytes();
    System.out.println(Arrays.toString(bytes));
    // 使用 gbk 进行编码
    byte[] gbks = str1.getBytes("gbk");
    System.out.println(Arrays.toString(gbks));
    String str2 = new String(bytes);
    System.out.println(str2);
    String str3 = new String(gbks, "gbk");
    System.out.println(str3);
}

与 StringBuffer、StringBuilder 之间的转换

String –> StringBuffer、StringBuilder:调用 StringBuffer、StringBuilder 的构造器

StringBuffer、StringBuilder -> String:

调用 String 的构造器

StringBuffer、StringBuilder 的 toString

JVM 中字符串常量池存放位置说明

JDK6.0:字符串常量池存储在方法区(永久区)

JDK7.0:字符串常量池存储在堆空间

JDK8.0:字符串常量池存储在方法区(元空间)

成绩算法题目的考查

模拟一个trim方法,去除字符串两端的空格。

public String myTrim(String str) {
    if (str != null) {
        // 用于记录从前往后首次索引位置不是空格的位置的索引
        int start = 0;
        // 用于记录从后往前首次索引位置不是空格的位置的索引
        int end = str.length() - 1;
        while (start < end && str.charAt(start) == ' ') {
            start++;
        }
        while (start < end && str.charAt(end) == ' ') {
            end--;
        }
        if (str.charAt(start) == ' ') {
            return "";
        }
        return str.substring(start, end + 1);
    }
    return null;
}

将一个字符串进行反转。将字符串中指定部分进行反转。比如“abcdefg”反转为”abfedcg”

/**
 * 方式一:转换为 char[]
 * @param str
 * @param startIndex
 * @param endIndex
 * @return
 */
public String reverseChar(String str, int startIndex, int endIndex){
    if (str != null) {
        char[] arr = str.toCharArray();
        for(int x = startIndex, y = endIndex; x < y; x++, y--){
            char temp = arr[x];
            arr[x] = arr[y];
            arr[y] = temp;
        }
        return new String(arr);
    }
    return null;
}
/**
 * 方式二:使用 String 的拼接
 * @param str
 * @param startIndex
 * @param endIndex
 * @return
 */
public String reverseString(String str, int startIndex, int endIndex){
    if (str != null) {
        String reverseStr = str.substring(0, startIndex);
        for(int i = endIndex; i >= startIndex; i--){
            reverseStr += str.charAt(i);
        }
        reverseStr += str.substring(endIndex + 1);
        return reverseStr;
    }
    return null;
}
/**
 * 方式三:使用 StringBuffer / StringBuilder 替换 String
 * @param str
 * @param startIndex
 * @param endIndex
 * @return
 */
public String reverseStringBuilder(String str, int startIndex, int endIndex){
    if (str != null) {
        StringBuilder builder = new StringBuilder(str.length());
        builder.append(str.substring(0, startIndex));
        for(int i = endIndex; i >= startIndex; i--){
            builder.append(str.charAt(i));
        }
        builder.append(str.substring(endIndex + 1));
    }
    return null;
}

获取一个字符串在另一个字符串中出现的次数。比如:获取“ab”在“abkkcadkabkebfkabkskab” 中出现的次数

/**
 * 获取 subStr 在 mainStr 中出现的次数
 * @param mainStr
 * @param subStr
 * @return
 */
public int getCount(String mainStr, String subStr){
    int mainLength = mainStr.length();
    int subLength = subStr.length();
    int count = 0;
    int index = 0;
    if (mainLength >= subLength){
        // 方式一
        /*while ((index = mainStr.indexOf(subStr)) != -1){
            count++;
            mainStr = mainStr.substring(index + subStr.length());
        }*/
        // 方式二
        while ((index = mainStr.indexOf(subStr, index)) != -1){
            count++;
            index += subLength;
        }
    }
    return count;
}

获取两个字符串中最大相同子串。比如 str1 = "abcwerthelloyuiodef“;str2 = “cvhellobnm”

提示:将短的那个串进行长度依次递减的子串与较长的串比较。

/**
 * 如果存在多个长度相同的最大相同子串
 * 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
 * @param str1
 * @param str2
 * @return
 */
public String[] getMaxSameSubString(String str1, String str2) {
    if (str1 != null && str2 != null) {
        StringBuffer sBuffer = new StringBuffer();
        String maxString = (str1.length() > str2.length()) ? str1 : str2;
        String minString = (str1.length() > str2.length()) ? str2 : str1;
        int len = minString.length();
        for (int i = 0; i < len; i++) {
            for (int x = 0, y = len - i; y <= len; x++, y++) {
                String subString = minString.substring(x, y);
                if (maxString.contains(subString)) {
                    sBuffer.append(subString + ",");
                }
            }
            // System.out.println(sBuffer);
            if (sBuffer.length() != 0) {
                break;
            }
        }
        String[] split = sBuffer.toString().replaceAll(",$", "").split("\\,");
        return split;
    }
    return null;
}

对字符串中字符进行自然顺序排序。

提示:

字符串变成字符数组。

对数组排序,选择,冒泡,Arrays.sort();

将排序后的数组变成字符串。

StringBuffer、StringBuilder

String、StringBuffer、StringBuilder 三者的比较

String:不可变的字符序列;底层使用 char[] 存储

StringBuffer:可变的字符序列;线程安全的,效率偏低;底层使用 char[] 存储

StringBuilder:可变的字符序列;JDK 5.0新增线程不安全,效率高;底层使用 char[] 存储

StringBuffer 与 StringBuilder 的内存解析

以 StringBuffer 为例

String str = new String(); // char[] value = new char[0];
String str = new String("abc"); // char[] value = new char[]{'a', 'b', 'c'};
StringBuffer sb1 = new StringBuffer(); // char[] value = new char[16];底层创建了一个长度是16的数组
sb1.append('a') // value[0] = 'a';
sb1.append('b') // value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc"); // char[] value = new char["abc".length() + 16];

问题1:System.out.println(sb2.length()); // 3


问题2:扩容问题:如果要添加的数据底层数组撑不下了,那就需要扩容底层的数组,默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中


指导意义:开发中建议使用:StringBuffer(int capacity) 或 StringBuilder(int capacity)


对比String、StringBuffer、StringBuilder 三者的执行效率

从高到低:StringBuilder > StringBuffer > String

StringBuffer、StringBuilder 中的常用方法

StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接

StringBuffer delete(int start,int end):删除指定位置的内容

StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str

StringBuffer insert(int offset, xxx):在指定位置插入 xxx

StringBuffer reverse():把当前字符序列逆转

int indexOf(String str)

String substring(int start,int end):返回一个 从 start 开始到 end 索引结束的左闭右开区间的子字符串

int length()

char charAt(int n)

void setCharAt(int n ,char ch)

总结:


增:append(xxx)

删:delete(int start, int end)

改:setCharAt(int n ,char ch) / replace(int start, int end, String str)

查:charAt(int n)

插:insert(int offset, xxx)

长度:length()

遍历:for + charAt() / toString()

JDK8 之前的日期时间 API

获取系统当前时间

System 类中的 currentTimeMillis():返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差,称为时间戳


java.util.Date 类和 java.sql.Date 类

两个构造器的使用

构造器一:Date():创建一个对应当前时间的 Date 对象

构造器二:Date(long date):创建指定毫秒数的 Date 对象

两个方法的使用

toString():显示当前的年月日时分秒

getTime():获取当前 Date 对象对应的毫秒数(时间戳)

java.sql.Date 对应着数据库自动日期类型的变量

如何实例化

如何将 java.util.Date 对象转换成 java.sql.Date 对象

@Test
public void test2(){
    // 构造器一:Date():创建一个对应当前时间的 Date 对象
    Date date1 = new Date();
    System.out.println(date1);
    System.out.println(date1.getTime());
    // 构造器二:Date(long date):创建指定毫秒数的 Date 对象
    Date date2 = new Date(1637809383273L);
    System.out.println(date2);
    java.sql.Date date3 = new java.sql.Date(99999323820232L);
    System.out.println(date3);
    Date date4 = new Date(221223445L);
    // java.sql.Date date5 = (java.sql.Date) date4;
    java.sql.Date date5 = new java.sql.Date(date3.getTime());
}

java.text.SimpleDateFormat 类

SimpleDateFormat 对日期 Date 类的格式化和解析

两个操作

格式化:日期 --> 字符串

解析:格式化的逆过程 字符串 --> 日期

SimpleDateFormat 的实例化:构造器

// 按照指定的方式格式化和解析:调用带参数的构造器
// SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyy.MMMMM.dd GGG hh:mm aaa");
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 格式化
System.out.println(sdf1.format(date));
// 解析:要求字符串必须符合 SimpleDateFormat 识别的格式(通过构造器参数体现),否则抛异常
System.out.println(sdf1.parse(sdf1.format(date)));

练习:

/**
 * 练习一:字符串“2020-09-08”转换为 java.sql.Date
 * 练习二:三天打鱼两天晒网 1990-01-01 xxxx-xx-xx 打鱼 晒网
 * 总天数 % 5 == 1, 2, 3 : 打鱼
 * 总天数 % 5 == 4, 0    : 晒网
 * 总天数的计算
 * 方式一:(date2.getTime() - date1.getTime()) / (1000 * 60 * 60 * 24)
 * 方式二:1990-01-01  --> 2019-12-31 + 2020-01-01 --> 2020-09-08
 */
@Test
public void testExer() throws ParseException {
    String birth = "2020-09-08";
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    Date date = simpleDateFormat.parse(birth);
    java.sql.Date birthDate = new java.sql.Date(date.getTime());
    System.out.println(birthDate);
}

Calendar 类:日历类、抽象类

@Test
public void testCalendar(){
    // 1.实例化
    // 方式一:创建其子类(GregorianCalendar)的对象
    // 方式二:调用其静态方法 getInstance()
    Calendar calendar = Calendar.getInstance();
    // System.out.println(calendar.getClass());
    // 2.常用方法
    // get()
    int days = calendar.get(Calendar.DAY_OF_MONTH);
    System.out.println(days);
    System.out.println(calendar.get(Calendar.DAY_OF_YEAR));
    // set()
    // Calender 可变性
    calendar.set(Calendar.DAY_OF_MONTH, 22);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
    // add()
    calendar.add(Calendar.DAY_OF_MONTH, -3);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
    // getTime():日历类 --> Date
    Date date = calendar.getTime();
    System.out.println(date);
    // setTime():Date --> 日历类
    Date date1 = new Date();
    calendar.setTime(date1);
    System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
}


目录
相关文章
|
22天前
|
JSON NoSQL Java
Redis入门到通关之Java客户端SpringDataRedis(RedisTemplate)
Redis入门到通关之Java客户端SpringDataRedis(RedisTemplate)
219 0
|
1天前
|
JavaScript 前端开发 Java
Go语言入门【java->go】
Go语言入门【java->go】
9 2
|
7天前
|
监控 Java 测试技术
性能工具之Java分析工具BTrace入门
【5月更文挑战第25天】性能工具之Java分析工具BTrace入门
18 2
|
10天前
|
Java 编译器
<JAVA> java入门面向0基础教程(数据类型,运算符)
<JAVA> java入门面向0基础教程(数据类型,运算符)
19 1
<JAVA> java入门面向0基础教程(数据类型,运算符)
|
11天前
|
Oracle Java 程序员
java基础篇-java入门认知
# Day01 —— Java基础入门概览 本文介绍了Java语言的背景知识、快速入门、开发工具以及基础语法。Java由Sun公司(现属Oracle)开发,创始人是詹姆斯·高斯林。Java可应用于桌面应用、企业级应用、移动应用、服务器系统和大数据开发等多个领域。Java技术体系包括Java SE(标准版)、Java EE(企业版)和Java ME(小型版)。 在开始编程前,需安装JDK,通过`javac`和`java`命令进行编译和运行。Java程序的执行依赖于Java虚拟机(JVM),实现跨平台运行。IDEA是常用的Java集成开发环境,提供代码提示、错误检查等功能,提高开发效率。
|
13天前
|
存储 Java 开发者
探索Java编程的奥秘:从入门到实践
本文是关于Java编程的简介,首先介绍了Java作为广泛应用的编程语言对初学者和专业人士的吸引力。接着,讲解了Java的基础概念,包括数据类型(如基本和引用类型)和变量,以及运算符和表达式。文章还提到了控制流语句,如条件语句和循环语句,用于控制程序执行流程。然后,转向面向对象编程,阐述了类与对象的概念,以及封装和继承的重要性。最后,简述了Java在Web开发、移动应用和桌面应用等领域的实际应用,并给出一个使用Swing创建简单GUI的示例。
|
13天前
|
前端开发 JavaScript Java
JAVA Web开发入门与实战
本文引导读者入门JAVA Web开发,介绍了Web开发的基本概念,如Servlet、JSP和JavaBean,并详细阐述了JAVA Web开发环境的搭建。文章通过一个在线书店系统的实战项目,展示了从需求分析、数据库设计到前后端开发的全过程,涵盖Servlet处理请求、JSP动态生成页面及表单添加书籍功能。最后,文章提及了进阶技术,如框架使用、前端集成和安全性考虑,鼓励读者深入探索JAVA Web开发的广阔世界。
|
20天前
|
算法 Java C++
刷题两个月,从入门到字节跳动offer丨GitHub标星16k+,美团Java面试题
刷题两个月,从入门到字节跳动offer丨GitHub标星16k+,美团Java面试题
|
21天前
|
算法 Java Python
保姆级Java入门练习教程,附代码讲解,小白零基础入门必备
保姆级Java入门练习教程,附代码讲解,小白零基础入门必备
|
22天前
|
SQL Java 关系型数据库
零基础轻松入门Java数据库连接(JDBC)
零基础轻松入门Java数据库连接(JDBC)
18 0