编码

ASCII:用八位二进制的低七位,一共规定了128个字符的编码,一个字节表示一个字符,
扩展ASCII:第八位为1,规定了以1开头的128个字符
Unicode:固定大小的编码,通常两个字节表示一个字符,字母和汉字统一用两个字节,浪费空间
UTF-8:是一种变长的编码方式。字母用一个字节,汉字用三个字节,是在互联网上使用最广的一中Unicode的实现方式
gbk:可以表示汉字,范围广,字母用一个字节,汉字用两个字节 ANSI编码 -- 不同地区采用的编码的统称
UTF-8编码规则:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
编码 大小 支持语言
ASCII 1个字节 英文
Unicode 2个字节(生僻字4个) 所有语言
UTF-8 1-6个字节,英文字母1个字节,汉字3个字节,生僻字4-6个字节 所有语言

基本语法

编写 Java 程序时,应注意以下几点:

  • 大小写敏感:Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的。
  • 类名:对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass
  • 方法名:所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写。
  • 源文件名:源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记 Java 是大小写敏感的),文件名的后缀为 .java。(如果文件名和类名不相同则会导致编译错误)。
  • 主方法入口:所有的 Java 程序由 public static void main(String[] args) 方法开始执行。

标识符

Java 所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。

关于 Java 标识符,有以下几点需要注意:

  • 所有的标识符都应该以字母(A-Z 或者 a-z),美元符($)、或者下划线(_)开始
  • 首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下划线(_)或数字的任何字符组合
  • 关键字和保留字不能用作标识符
  • 标识符是大小写敏感的
  • 标识符不能包含空格
  • 合法标识符举例:age、$salary、_value、__1_value
  • 非法标识符举例:123abc、-salary

内置数据类型

Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。

byte:

  • byte 数据类型是8位、有符号的,以二进制补码表示的整数;
  • 最小值是 -128(-2^7)
  • 最大值是 127(2^7-1)
  • 默认值是 0
  • byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一;
  • 例子:byte a = 100,byte b = -50。

short:

  • short 数据类型是 16 位、有符号的以二进制补码表示的整数
  • 最小值是 -32768(-2^15)
  • 最大值是 32767(2^15 - 1)
  • Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一;
  • 默认值是 0
  • 例子:short s = 1000,short r = -20000。

int:

  • int 数据类型是32位、有符号的以二进制补码表示的整数;
  • 最小值是 -2,147,483,648(-2^31)
  • 最大值是 2,147,483,647(2^31 - 1)
  • 一般地整型变量默认为 int 类型;
  • 默认值是 0
  • 例子:int a = 100000, int b = -200000。

long:

  • long 数据类型是 64 位、有符号的以二进制补码表示的整数;

  • 最小值是 -9,223,372,036,854,775,808(-2^63)

  • 最大值是 9,223,372,036,854,775,807(2^63 -1)

  • 这种类型主要使用在需要比较大整数的系统上;

  • 默认值是 0L

  • 例子: long a = 100000L,Long b = -200000L。
    "L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。

    //注意:java对于整型的默认类型为int,数的末尾要加上L
    long n1 = 100L;  
    

float:

  • float 数据类型是单精度、32位、符合IEEE 754标准的浮点数;

  • float 在储存大型浮点数组的时候可节省内存空间;

  • 默认值是 0.0f

  • 浮点数不能用来表示精确的值,如货币;

  • 例子:float f1 = 234.5f。

    //Java对于浮点数的默认类型为double
    float f1 = 1.1f;  //末尾的f不可省,否则相当于八个字节的数据放到四个字节,编译会报错
    

double:

  • double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数;

  • 浮点数的默认类型为double类型;

  • double类型同样不能表示精确的值,如货币;

  • 默认值是 0.0d

  • 例子:double d1 = 123.4。

    System.out.println(8.1/3);     //结果为2.6999999999999997
    //尽量不要直接判断两个浮点数是否相等,应该以两个数的差值的绝对值是否在某个精度范围内来判断两个浮点数是否相等
    System.out.println(5.2e1);        //结果为52.0  注意末尾的0不可以省略
    

boolean:

  • boolean数据类型表示一位的信息;

  • 只有两个取值:true 和 false;

  • 这种类型只作为一种标志来记录 true/false 情况;

  • 默认值是 false

  • 例子:boolean one = true。

    不可以用0/1代替false/true,只有两个值

char:

  • char类型是一个单一的 16 位 Unicode 字符 两个字节;
  • 最小值是 \u0000(即为0);
  • 最大值是 \uffff(即为65,535);
  • char 数据类型可以储存任何字符;
  • 例子:char letter = 'A';。

注:字符型的本质,其实是字符对应的ASCII编码

自动类型转换

  • 有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的那种数据类型,然后再进行计算

    int n1 = 10;
    float t1 = n1 + 1.1;   //错误,系统会将n1 + 1.1的结果转为默认的double类型,将8字节的double赋给4字节的float会报错
    float t2 = n1 + 1.1F;  //正确
    
  • 当把精度大的数据类型赋给精度小的数据类型时,就会报错,反之就会进行自动类型转换

  • (byte , short ) 和 char 之间不会相互自动类型转换

    byte b1 = 10;   //当把数赋给byte时,先判断该数是否在byte范围内[-128,127],在就没问题
    int n2 = 1;
    byte b2 = n2;   //错误,只有赋的是具体的数时,才会先判断该数是否在byte范围内
    char c1 = b1;   //char与byte之间不能相互自动类型转换
    short s1 = b1;  //short与byte之间同样也不能相互自动类型转换
    
  • byte , short , char 他们三者之间可以计算,在计算时首先转换为int类型

    byte b1 = 1;
    byte b2 = 2;
    short s1 = b1 + b2;  //错误,计算后的结果为int
    char c1 = b1 + b2;   //错误,原因同上
    int s2 = b1 + b2;    //correct
    
  • Boolean类型不参与自动类型转换

  • 自动提升原则,表达式结果的类型自动提升为操作数中精度最高的类型

强制类型转换

当进行数据精度大小 由大到小的转换时,就需要用到强制类型转换

int n1 = (int)1.9;
System.out.println("n1="+n1);   //n1=1
int n2 = 2000;
byte b1 = (byte)n2;
System.out.println("b1="+b1);    //b1=-48        发生溢出

强制类型强转只针对最近的操作数有效,往往会用小括号提高优先级

int x = (int)10*3.5+2.4;      //错误,不能将double转换为int
int x = (int)(10*3.5+2.4);    //正确 37.4->37

char类型可以保存int的常量值,但不能保存int的变量值,需要强转

byte ,short 和 char 类型在进行运算时,当做int类型处理

基本类型与字符串的转换?

//基本数据类型转字符串
int n1 = 100;
String s1 = n1 + "";  //+"" 即可将任意基本数据类型转为字符串
//字符串转基本类型  使用基本类型对应的包装类的相应方法,得到基本数据类型
String s2 = "123";
int n2 = Integer.parseInt(s2);  //使用基本数据类型对应的包装类,将String转为int,利用Integer.parseInt
boolean b1 = Boolean.parseBoolean("true"); //将String转为Boolean类型
//注意,如果s2="hello",将其转换为int类型,编译时不会出错,但是执行时会抛出异常Exception导致程序终止
//字符串加一个整形数据
String s1 = "abc";
System.out.println(s1 + 1);   //  结果为abc1

字符串的内容比较使用字符串的equal方法

==判断是两个字符串是否在同一块内存空间,而equal判断的是内容是否相等

String name = scan.next();
System.out.println("SVicen",equals(name));   //比较传入的字符串name是否等于SVicen
string[] names = {"金毛狮王",“白眉鹰王","紫衫龙王“};
    for (int i = 0; i <names.length; i++) {
    if(name.equals(names[i])) {
          System.out.println("找到了");
    }
}

引用类型

  • 在Java中,引用类型的变量非常类似于C/C++的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Employee、Puppy 等。变量一旦声明后,类型就不能被改变了。
  • 对象、数组都是引用数据类型。
  • 所有引用类型的默认值都是null。
  • 一个引用变量可以用来引用任何与之兼容的类型。
  • 例子:Site site = new Site("Nowcoder")。

常量

常量在程序运行时是不能被修改的。

在 Java 中使用 final 关键字来修饰常量,声明方式和变量类似:

final double PI = 3.1415927;

虽然常量名也可以用小写,但为了便于识别,通常使用大写字母表示常量。

字面量可以赋给任何内置类型的变量。例如:

byte a = 68;
char a = 'A'

byte、int、long、和short都可以用十进制、16进制以及8进制的方式来表示。

当使用字面量的时候,前缀 0 表示 8 进制,而前缀 0x 代表 16 进制, 例如:

int decimal = 100;
int octal = 0144;
int hexa =  0x64;

和其他语言一样,Java的字符串常量也是包含在两个引号之间的字符序列。下面是字符串型字面量的例子:

"Hello World"
"two\nlines"
"\"This is in quotes\""    // \"将"转义为",若不加",会将字符串内容认为前两个双引号的内容,后面的内容报错

字符串常量和字符变量都可以包含任何 Unicode 字符。例如:

char a = '\u0001';
String a = "\u0001";

转义字符

符号 字符含义
\n 换行 (0x0a)
\r 回车 (0x0d)
\f 换页符(0x0c)
\b 退格 (0x08)
\0 空字符 (0x20)
\s 字符串
\t 制表符
" 双引号
' 单引号
\ 反斜杠
\ddd 八进制字符 (ddd)
\uxxxx 16进制Unicode字符 (xx

注:\r 回车符的意思并不是换行,而是将光标移动到当前行的开始位置 详见下列

System.out.println("helloworld\r北京");
//输出结果为北京oworld    因为一个汉字占两个字节

变量类型

Java语言支持的变量类型有:

  • 类变量:独立于方法之外的变量,用 static 修饰。
  • 实例变量:独立于方法之外的变量,不过没有 static 修饰。
  • 局部变量:类的方法中的变量。
public class Variable{
    static int allClicks=0;    // 类变量
    String str="hello world";  // 实例变量
    public void method(){
        int i =0;  // 局部变量
    }
}

接收用户输入

import java.util.Scanner;
//1.引入Scanner类(简单文本扫描器)所在的包
//2.创建Scanner对象
Scanner myScanner = new Scanner(System.in);
System.out.println("请输入名字");
String name = myScanner.next();  //接收用户输入
System.out.println("请输入年龄");
int age = myScanner.nextInt();
System.out.println("名字:" + name + "\n年龄:" + age);
myScanner.close();   //使用完后应该close掉
import java.util.Scanner;
public class ScannerDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        // 从键盘接收数据
        // next方式接收字符串
        System.out.println("next方式接收:");
        // 判断是否还有输入
        if (scan.hasNext()) {
            String str1 = scan.next();  
            System.out.println("输入的数据为:" + str1);
        }
        scan.close();
    }
}

读入char型数据char ch = scan.next().charAt(0); --返回指定索引处的 char 值,这里的索引就是0

char ch = scan.next().charAt(0); --如果输入的是"你好",输出结果为''你''

next() 与 nextLine() 区别

next():

  • 1、一定要读取到有效字符后才可以结束输入。
  • 2、对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
  • 3、只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
  • next() 不能得到带有空格的字符串。

nextLine():

  • 1、以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。
  • 2、可以获得空白。

如果要输入 int 或 float 类型的数据,在 Scanner 类中也有支持,但是在输入之前最好先使用 hasNextXxx() 方法进行验证,再使用 nextXxx() 来读取:

流程控制

分支控制

if...else if...else 语句

if 语句后面可以跟 else if…else 语句,这种语句可以检测到多种可能的情况。

使用 if,else if,else 语句的时候,需要注意下面几点:

  • if 语句至多有 1 个 else 语句,else 语句在所有的 else if 语句之后。
  • if 语句可以有若干个 else if 语句,它们必须在 else 语句之前。
  • 一旦其中一个 else if 语句检测为 true,其他的 else if 以及 else 语句都将跳过执行。
boolean b = true;
if (b = false) {         //注意这里将b赋值为false  此时第一个if的判断语句为false,不会执行
    System.out.println("a");
} else if(b) {
    System.out.println("b");
} else if(!b) {            //这里判断为true,会执行,最终输出结果为 c
    System.out.println("c");
} else {
    System.out.println("d");
}

switch case 语句

switch(expression){
    case value :
       //语句
       break; //可选,一般加上
    case value :
       //语句
       break; 
    default : //可选
       //语句
}

switch case 语句有如下规则:

  • switch 语句中的变量类型可以是: byte、short、int 、enum或者 char。(注意不可以使float或double),从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
  • switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。
  • case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或者字面常量。
  • 当变量的值与 case 语句的值相等时,那么 case 语句之后的语句开始执行,直到 break 语句出现才会跳出 switch 语句。
  • 当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。
  • switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。

switch case 执行时,一定会先进行匹配,匹配成功返回当前 case 的值,再根据是否有 break,判断是否继续输出,或是跳出判断。

char c = 'a';
switch(c){
    case 'a' :
       System.out.println("ok1");
       break;
    case 65 :    //这样是可以的,字符型实质上就是int
       System.out.println("ok2");
       break; 
    default :
       System.out.println("ok3");
}

循环结构

for循环

关于 for 循环有以下几点说明:

  • 最先执行初始化步骤。可以声明一种类型,但可初始化一个或多个循环控制变量,也可以是空语句。
  • 然后,检测布尔表达式的值。如果为 true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。
  • 执行一次循环后,更新循环控制变量。
  • 再次检测布尔表达式。循环执行上面的过程。

增强 for 循环--Java5中引入

for(声明语句 : 表达式) {
   //代码句子
}

声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等。

表达式:表达式是要访问的数组名,或者是返回值为数组的方法。

public class Test {
   public static void main(String args[]){
      int [] numbers = {10, 20, 30, 40, 50};
      for(int x : numbers ){
         System.out.print( x );
         System.out.print(",");
      }
      System.out.print("\n");
      String [] names ={"James", "Larry", "Tom", "Lacy"};
      for( String name : names ) {
         System.out.print( name );
         System.out.print(",");
      }
   }
}
//运行结果为
//10,20,30,40,50,
//James,Larry,Tom,Lacy,

break 关键字

break 主要用在循环语句或者 switch 语句中,用来跳出整个语句块。

break 跳出最里层的循环,并且继续执行该循环下面的语句。

可以配合label使用, break label1; //跳出label层循环

数组

  • 数组中的元素可以使任意数据类型,包括基本类型和引用类型。但是不能混用

  • 数组创建后,如果没有赋值,有默认值int,short,byte,long->0, float,double->0.0, char->\u0000, boolean->false,String->null

  • 数组属于引用类型,数组型数据是对象

  • 数组的赋值为引用传递,传递的是一个地址

    int[] arr1 = {1,2,3,4};
    int[] arr2 = arr1;
    arr2[0] = 5;  //修改arr2的值,arr1的值也会发生改变
    

声明数组变量

dataType[] arrayRefVar;   // 首选的方法
dataType arrayRefVar[];  // 效果相同,但不是首选方法

创建数组

Java语言使用new操作符来创建数组,语法如下:

arrayRefVar = new dataType[arraySize];

上面的语法语句做了两件事:

  1. 使用 dataType[arraySize] 创建了一个数组。
  2. 把新创建的数组的引用赋值给变量 arrayRefVar。

数组变量的声明,和创建数组可以用一条语句完成,如下所示:

dataType[] arrayRefVar = new dataType[arraySize];

还可以使用如下的方式创建数组(静态初始化):

dataType[] arrayRefVar = {value0, value1, ..., valuek};

数组扩容

int[] arr1 = {1,2,3};
//要在arr1后加一个元素,注意不可以直接arr[3] = 4;  --发生数组下标越界
int[] newArr = new int[arr1.length + 1];
for (int i = 0; i < arr1.length; i++){
    newArr[i] = arr1[i];
}
newArr[newArr.length - 1] = 4;
arr1 = newArr;   //将扩容后的数组赋给原来的数组

For-Each 循环

又叫加强型循环,能在不使用下标的情况下遍历数组。

public class TestArray {
   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};
      // 打印所有数组元素
      for (double element: myList) {
         System.out.println(element);
      }
   }
}

多维数组

多维数组可以看成是数组的数组,比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组,例如:

String str[][] = new String[3][4];
int[][] y 或者 int y[][] 或者 int []y[];
int[][] a= {{1, 2, 3}, {4, 5, 6},{7, 8, 9}};
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        System.out.print(a[i][j] + " ");
    }
    System.out.println("");
}
System.out.println(a[0][2]);   //注意java语言 这里不可以输出 a[0][3] 而c++可以,输出的为第二行第一列的4
//由此可见c++的多维数组内存存储空间是连续的,而java的多维数组内存空间是不连续的

Java的多维数组的每个数组的数据个数可以不同,因为每个一维的数组可以单独new一定的空间

多维数组的动态初始化(以二维数组为例)

1.直接为每一维分配空间,格式如下:

type[][] typeName = new type[typeLength1][typeLength2];
例如
int a[][] = new int[2][3];
int[][] arr = {{1},{1,2},{1,2,3}};

2.从最高维开始,分别为每一维分配空间,例如:

String[] strs = new String[] { "a", "b", "c" };  //是正确的  strs其实是一维的,只不过直接静态赋值了
//注意这里String[] strs = new String[3] { "a", "b", "c" }  就是错误的
String s[][] = new String[2][];
s[0] = new String[2];   //每一个一维的数组都需要 使用new运算符,否则其为空-无法存放数据
s[1] = new String[3];
//注意这里 s[0]中两个元素,而s[1]中有三个元素
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");

s[0]=new String[2]s[1]=new String[3] 是为最高维分配引用空间,也就是为最高维限制其能保存数据的最长的长度,然后再为其每个数组元素单独分配空间 s0=new String("Good") 等操作。

Arrays 类

ava.util.Arrays 类能方便地操作数组,它提供的所有方法都是静态的。

具有以下功能:

  • 给数组赋值:通过 fill 方法。
  • 对数组排序:通过 sort 方法,按升序。
  • 比较数组:通过 equals 方法比较数组中元素值是否相等。
  • 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。
序号 方法和说明
1 public static int binarySearch(Object[] a, Object key) 用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。
2 public static boolean equals(long[] a, long[] a2) 如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。
3 public static void fill(int[] a, int val) 将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。
4 public static void sort(Object[] a) 对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

JAVA内存结构

  • 栈:一般存放基本数据类型(局部变量)和类的方法
  • 堆:存放引用型数据,对象(对象的各个属性的地址)、数组(通过new创建的数据类型)
  • 方法区:常量池(存放常量比如字符串),类加载信息(具体对象的各个属性的值)

类与对象

一个类可以包含以下类型变量:

  • 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
  • 成员变量:又叫全局变量,全局变量又分为类变量(静态变量)、实例变量两种。成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
  • 类变量:也称为静态变量,类变量也声明在类中,方法体之外,但必须声明为 static 类型

局部变量

  • 局部变量声明在方法、构造方法或者语句块中;
  • 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁;
  • 访问修饰符不能用于局部变量
  • 局部变量只在声明它的方法、构造方法或者语句块中可见;
  • 局部变量是在栈上分配的
  • 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。

实例变量?

  • 实例变量声明在一个类中,但在方法、构造方法和语句块之外,没有 static 修饰;
  • 当一个对象被实例化之后,每个实例变量的值就跟着确定;
  • 实例变量在对象创建的时候创建,在对象被销毁的时候销毁;
  • 实例变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息;
  • 实例变量可以声明在使用前或者使用后;
  • 访问修饰符可以修饰实例变量;
  • 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见;
  • 实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。变量的值可以在声明时指定,也可以在构造方法中指定;
  • 实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName

类变量(静态变量)

  • 类变量也称为静态变量,在类中以 static 关键字声明,但必须在方法之外。
  • 无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。
  • 静态变量除了被声明为常量外很少使用。常量是指声明为public/private,final和static类型的变量。常量初始化后不可改变。
  • 静态变量储存在静态存储区。经常被声明为常量,很少单独使用static声明变量。
  • 随着类的加载而存在,随着类的消失而消失。静态变量在第一次被访问时创建,在程序结束时销毁。
  • 与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为public类型。
  • 默认值和实例变量相似。数值型变量默认值是0,布尔型默认值是false,引用类型默认值是null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。
  • 静态变量可以通过:ClassName.VariableName的方式访问。
  • 类变量被声明为public static final类型时,类变量名称一般建议使用大写字母。如果静态变量不是public和final类型,其命名方式与实例变量以及局部变量的命名方式一致。

?属性和局部变量可以重名,访问时遵循就近原则

?属性随着对象的创建而创建,随着对象的销毁而销毁,其实非静态属性就是实例变量

?全局变量/属性:可以被本类使用,也可以被其他类使用,创建对象的时候实例化

?全局变量/属性可以加修饰符,但局部变量不可以加修饰符

构造方法/构造器

构造器是对对象进行初始化的而不是创建对象的

每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法进行对象的初始化。

在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,且没有返回值。也不写void。

一旦定义了自己的构造器,系统默认的构造器就被覆盖了,不能使用,除非显示的再定义一下默认无参构造器

下面是一个构造方法示例:

public class Puppy{
    public Puppy(){
    }
    public Puppy(String name){
        // 这个构造器仅有一个参数:name
    }
}

反汇编指令javap 类名(.class后缀) 可以加 -c -v 选项

image-20220412213329202

this

-- 与对象关联,实际上每个对象在堆区分配空间的时候就隐式分配了一个this指向它自己

使用this输出的值一定是属性值,但是如果不加this,如果有局部变量与属性同名,则会就近输出局部变量

public static void main(String[] args) {
    T t1 = new T();
}
class T {
    public T() {
        //对this的调用必须在构造器的第一条语句
        //访问构造器语法:this(参数列表) 注意只能在一个构造器中访问另一个构造器  且this语句必须放在第一条
        this("SVicen", 20);   //在一个构造器内调用其他的构造器
        System.out.println("T()构造器调用");
    }
    public T(String name, int age) {
        System.out.println("T(String name, int age)构造器调用");
    }
}
//输出结果如下
T(String name, int age)构造器调用
T()构造器调用

IDEA自定义模板 settings-> editor -> live templates

创建对象

对象是根据类创建的。在Java中,使用关键字 new 来创建一个新的对象。创建对象需要以下三步:

  • 声明:声明一个对象,包括对象名称和对象类型。
  • 实例化:使用关键字 new 来创建一个对象。
  • 初始化:使用 new 创建对象时,会调用构造方法初始化对象。
public class Puppy{
   int age = 10;
   String name;
   public Puppy(String n,int a){  //构造函数
      name = n;
      age = a;
      //这个构造器仅有一个参数:name
      System.out.println("小狗的名字是 : " + name ); 
   }
   public static void main(String[] args){ 
      // 下面的语句将创建一个Puppy对象
      Puppy myPuppy = new Puppy( "tommy",18 );
   }
}
//结果为:小狗的名字是 : tommy

匿名对象

  • 只可以使用一次
public class Circle {
    double radius; //半径
    public Circle(double r) {
        radius = r;
    }
    public double area() {
        return Math.PI * radius * radius;
    }
    public double len() {
        return 2 * Math.PI * radius;
    }
    public double test() {
        double a = 1.0;
        System.out.println(2 * a);
    }
    public static void main(String[] args) {
        new Circle.test();
    }
}

对象创建流程详解(以上代码为例)?

  • Puppy myPuppy = new Puppy( "tommy" )后 先在方法区加载类的属性和方法信息 ---只会加载一次
  • 在堆开辟空间(根据对象的属性个数)
  • 对对象进行默认初始化(Sting型为null,int型为0,boolean为false,char型为空),age初始化为0,name初始化为null
  • 进行类的定义中对属性的显式初始化,age = 10;name = null
  • 进行构造器的初始化,将传入的参数赋值给创建的对象的属性对应的堆空间
  • 将创建的对象的地址返回给创建的对象myPuppy (对象的引用)

对象的赋值

public class Object {
    public static void main(String[] args) {
        //创建Person对象
        Person p1 = new Person(20,'男',"SVicen");
        System.out.println("姓名为:" + p1.name + "   性别为:" + p1.gender + "   年龄为:" + p1.age);
        Person p2 = p1; //注意这里传值,传的其实是引用,修改p1,p2任意一个的值都会导致两个都发生变化
        p2.age = 18;             //但是如果令p2=null,只是将p2指向的地址改为了null,并不影响p1的值
        p1.gender = '女';   
        System.out.println("=====第一个人的属性如下=====");
        System.out.println("姓名为:" + p1.name + "   性别为:" + p1.gender + "   年龄为:" + p1.age);
        System.out.println("=====第二个人的属性如下=====");
        System.out.println("姓名为:" + p2.name + "   性别为:" + p2.gender + "   年龄为:" + p2.age);
    }
}
class Person {
    int age;
    char gender;
    String name;
    Person(int a, char g, String s) {
        this.age = a;
        this.gender = g;
        this.name = s;
    }
}
//输出结果如下
姓名为:SVicen   性别为:男   年龄为:20
=====第一个人的属性如下=====
姓名为:SVicen   性别为:女   年龄为:18
=====第二个人的属性如下=====
姓名为:SVicen   性别为:女   年龄为:18

源文件声明规则

在本节的最后部分,我们将学习源文件的声明规则。当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。

  • 一个源文件中只能有一个public类
  • 一个源文件可以有多个非public类
  • 源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
  • 如果一个类定义在某个包中,那么package语句应该在源文件的首行。
  • 如果源文件包含import语句,那么应该放在package语句和类定义之间。如果没有package语句,那么import语句应该在源文件中最前面。
  • import语句和package语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。

类有若干种访问级别,并且类也分不同的类型:抽象类和final类等。这些将在访问控制章节介绍。

除了上面提到的几种类型,Java还有一些特殊的类,如:内部类、匿名类。

Java包

包主要用来对类和接口进行分类。当开发Java程序时,可能编写成百上千的类,因此很有必要对类和接口进行分类。

IDEA,新建packet,名字为com.xiaoming -- 会创建二级目录xiaoming,以及目录com

包的命名一般为:com.公司名.项目名.业务模块名

包的作用

  • 1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
  • 2、区分相同名字的类,同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突
  • 3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类

Java 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类(class)、接口、枚举(enumerations)和注释(annotation)等。

包语句的语法格式为:

//例如 它的路径应该是 net/java/util/Something.java 这样保存的
package net.java.util;   // 一个类中只能有一个packeg
public class Something{
   ...
}

一个包(package)可以定义为一组相互联系的类型(类、接口、枚举和注释),为这些类型提供访问保护和命名空间管理的功能。

开发者可以自己把一组类和接口等打包,并定义自己的包。而且在实际开发中这样做是值得提倡的,当你自己完成类的实现之后,将相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和注释等是相关的。

由于包创建了新的命名空间(namespace),所以不会跟其他包中的任何名字产生命名冲突。使用包这种机制,更容易实现访问控制,并且让定位相关类更加简单。引用时只可以引入一个包,否则编译器无法区分。

以下是一些 Java 中的包:

  • java.lang-----打包基础的类,默认引入
  • java.io-----包含输入输出功能的函数
  • java.util.*-----util包,系统提供的工具包,工具类,使用Scanner
  • java.net.*-----网络包,网络开发
  • java.awt.*----做java的界面开发,GUI
创建包

创建包的时候,你需要为这个包取一个合适的名字。之后,如果其他的一个源文件包含了这个包提供的类、接口、枚举或者注释类型的时候,都必须将这个包的声明放在这个源文件的开头。

包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都应用于它。

如果一个源文件中没有使用包声明,那么其中的类,函数,枚举,注释等将被放在一个无名的包(unnamed package)中。

在 animals 包中加入一个接口(interface):

/* 文件名: Animal.java */
package animals;
interface Animal {
   public void eat();
   public void travel();
}

接下来,在同一个包中加入该接口的实现:

package animals;
/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{
   public void eat(){
      System.out.println("Mammal eats");
   }
   public void travel(){
      System.out.println("Mammal travels");
   } 
   public int noOfLegs(){
      return 0;
   }
   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

可变参数?

注:当有多个参数时,可变参数需在最后一个参数的位置,否则无法确定个数,同时一个函数无法有多个类型的可变参数

class Method{
    public int sum(int... nums) {
        int res = 0;
        for (int i = 0; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }
    public void f1(double... nums,double d1) {
        //这样写是错误的
    }
    //使用可变参数时,可以当做数组使用,即nums当做数组
    Method m = new Method();
    System.out.println(m.sum(5,10,100));
    System.out.println(m.sum(5,10,20,50,100));  //可以输入任意参数个数
}

Java 修饰符

Java语言提供了很多修饰符,主要分为以下两类:

  • 访问修饰符
  • 非访问修饰符

修饰符用来定义类、方法或者变量,通常放在语句的最前端。

访问控制修饰符?

Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

  • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
修饰符 当前类 同一包内 子孙类(同一包) 子孙类(不同包) 其他包
public Y Y Y Y Y
protected Y Y Y Y/N N
default Y Y Y N N
private Y N N N N

封装

在面向对象程式设计方法中,封装(Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
要访问该类的代码和数据,必须通过严格的接口控制。
封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。
适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

封装的优点
  • 良好的封装能够减少耦合。 高内聚,低耦合
  • 类内部的结构可以自由修改。
  • 可以对成员变量进行更精确的控制,保证数据安全合理
  • 隐藏实现细节
封装实现步骤
  • 属性私有化
  • 提供一个公共的public set方法,用于对属性判断并赋值
  • 提供一个公共的public get方法,用于获取属性的值

继承

继承的概念

继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

类的继承格式

在 Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:

class 父类 {
}
class 子类 extends 父类 {
}
继承的构造器调用?

当创建子类对象时,不管是用子类的哪个构造器,默认情况下都会去调用父类的无参构造器(通过一个super()函数 ),如果父类没有提供无参构造器(定义了有参构造器而没有对无参构造器进行显示的声明),则必须**在子类的构造器中显示用super(构造器形参列表) 来指明调用父类的有参构造器来完成父类的初始化。 **

super()与this()类似,都是只能应用在构造器里,且必须放在构造器的第一行。但可以在普通非静态方法中通过super.eat()调用父类的非静态方法。

继承类型

Java 不支持多继承,但支持多重继承。

Java学习笔记

继承的特性
  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
继承关键字

继承可以使用 extendsimplements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承object(这个类在 java.lang 包中,所以不需要 import)祖先类。

extends关键字

在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。

public class Animal {
    private String name;   
    private int id; 
    public Animal(String myName, String myid) { 
        //初始化属性值
    } 
    public void eat() {  //吃东西方法的具体实现  } 
    public void sleep() { //睡觉方法的具体实现  } 
} 
public class Penguin  extends  Animal{ 
}
implements关键字

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

public interface A {
    public void eat();
    public void sleep();
}
public interface B {
    public void show();
}
public class C implements A,B {
}
super关键字?

super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super取访问爷爷类的成员(前提是直接父类中没有同名成员),如果多个上级类中都有同名的成员,使用super访问遵循就近原则,

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}
class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    this.eat();   // this 调用自己的方法
    super.eat();  // super 调用父类方法
  }
}
final关键字

final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:

继承的内存布局?

创建son对象时,father和grandpa对象都会进行构造且放在一块内存,对于father或grandpa的private属性son是不可以访问的

son s = new son();
//通过son访问名字时,如果子类有该属性,则访问子类的,否则访问父类的,知道Object类(最终父类)
s.name;     //大头儿子
s.age;        //爸爸的年龄 39
s.hobby;    //爷爷的爱好 旅游

image-20220413184449013

public class ExtendExercise {
    public static void main(String[] args) {
        B b = new B();  //创建子类对象
    }
}
class A{
    A(){
        System.out.println("a");
    }
    A(String name) {
        System.out.println("a name");
    }
}
class B extends A{
    B(){
        //调用自己的有参构造R
        this("abc");
        System.out.println("b");
    }
    B(String name) {
        //默认有一个 super
        System.out.println("b name");
    }
}
//输出结果为
a
b name
b

重写(Override)

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
}
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
      a.move();// 执行 Animal 类的方法        动物可以移动
      b.move();//执行 Dog 类的方法            狗可以跑和走
   }
}

在上面的例子中可以看到,尽管b属于Animal类型,但是它运行的是Dog类的move方法。

这是由于在编译阶段,只是检查参数的引用类型(最前面声明的类型)。

然而在运行时,Java虚拟机(JVM)指定对象的类型并且运行该对象的方法。

因此在上面的例子中,之所以能编译成功,是因为Animal类中存在move方法,然而运行时,运行的是特定对象的方法。

class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
   public void bark(){
      System.out.println("狗可以吠叫");
   }
}
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
      a.move();// 执行 Animal 类的方法
      b.move();//执行 Dog 类的方法
      b.bark();
   }
}

这里,该程序将抛出一个编译错误,因为b的引用类型Animal(最左边的声明类型) 没有bark方法,编译不成功。

方法的重写规则?
  • 参数列表必须完全与被重写方法的相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。 比如说子类返回String(引用型),父类返回Object
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。 默认的权限比protected低,比private高。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写
  • 如果不能继承一个方法,则不能重写这个方法。
重写与重载之间的区别
区别点 重载方法 重写方法
参数列表 必须修改 一定不能修改
返回类型 可以修改 一定不能修改
发生范围 本类 父子类
异常 可以修改 可以减少或删除,一定不能抛出新的或者更广的异常
访问修饰符 可以修改 一定不能做更严格的限制(可以降低限制)

多态?

多态:方法或对象具有多种形态,是OOP的第三大特征,建立在封装和继承基础之上。

多态的优点
  • 消除类型之间的耦合关系
  • 可替换性
  • 可扩充性
  • 接口性
  • 灵活性
  • 简化性
  • 可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
多态存在的三个必要条件
  • 继承
  • 重写
  • 父类引用指向子类对象
对象的多态
  • 一个对象的编译类型和运行类型可以不一致
  • 编译类型在定义多态时就确定了,不能改变,而运行类型是可以改变的(类似于c++的父类指针指向子类对象)
  • 编译类型看定义时 =号 的左边,运行类型看 =号 的右边,可以通过Object的getClass方法查看运行类型
多态的向上转型?

父类的引用指向了子类对象(子类的对象向上转型)

?当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

写代码时看的是编译类型,可以调用编译类型的所有可访问的成员,不能调用子类特有的成员

?写代码时能调用的方法和属性都是编译类型的,但运行时调用的方法时从子类开始找的

调用方法时,从子类(运行类型)向上查找方法调用,(有可能父类定义的函数子类未定义 --但要注意这就不是多态了)

?调用方法时,体现多态--其实就是子类对父类的方法做了具体的实现,所以应该从子类开始找方法。

修改后的父类可以调用父类的所有成员(访问权限满足),但是不能调用子类特有的成员(父类中必须有对应的接口)?

Animal ani = new Animal();//             编译类型为Animal  运行类型为Animal
ani = new Dog();    //父类引用指向Dog子类    编译类型为Animal  运行类型为 Dog
ani = new Cat();    //父类引用指向Cat子类   编译类型为Animal  运行类型为 Cat
ani.eat();    //注意这里先看子类的方法,运行时关注运行类型,输出的是猫吃鱼,如果子类没有eat方法再去调用父类的
public class Test {
    public static void main(String[] args) {
      show(new Cat());  // 以 Cat 对象调用 show 方法
      show(new Dog());  // 以 Dog 对象调用 show 方法
      Animal a = new Cat();  // 向上转型  
      a.eat();               // 调用的是 Cat 的 eat
      Cat c = (Cat)a;        // 向下转型  
      c.work();        // 调用的是 Cat 的 work
  }  
    public static void show(Animal a)  {
      a.eat();  
        // 类型判断
        if (a instanceof Cat)  {  // 猫做的事情  instanceof用于判断对象的运行类型是否为Cat类型或Cat类型的子类型
            Cat c = (Cat)a;  
            c.work();  
        } else if (a instanceof Dog) { //狗做的事情 instanceof用于判断对象的运行类型是否为Dog类型或Dog类型的子类型
            Dog c = (Dog)a;  
            c.work();  
        }  
    }  
}
abstract class Animal {  
    abstract void eat();    //abstract  抽象类
}  
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
    public void work() {  
        System.out.println("抓老鼠");  
    }  
}  
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}
//输出结果
吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠
多态的向下转型

其实就是把执行子类对象的父类引用,转为执行子类对象的子类引用

  • 语法:子类类型 引用名 = (子类类型) 父类引用 Cat c = (Cat) animal
  • 只能强转父类的引用,不能强转父类对象 ?
  • 要求父类的引用必须指向当前要转型后的类型的对象,即上面的animal必须指向cat子类对象 ?
  • 当向下转型后,就可以调用子类型中的所有成员

属性没有重写之说, --看编译类型

public class Test {
    public static void main(String[] args) {
        Base base = new Sub(); //向上转型
        System.out.println(base.count);  //看编译类型,为Base,结果为10
        Sub sub = new Sub();
        System.out.println(sub.count);    //编译类型为Sub,  结果为20
    }
}
class Base{
    int count = 10;
}
class Sub{
    int count = 20;
}

instanceOf 比较操作符,用于判断对象的类型是否为XX类型或XX类型的子类型。

动态绑定机制DynamicBinding
  • 当调用对象方法时,该方法会和该对象的内存地址(运行类型)绑定
  • 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用

?详情见com.poly.dynamic 例子以及多态数组的实现

多态数组内会用到 instanceof 操作符

多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型 详见com\poly\polyexercise\TestPolyParameter.java

Object类(顶级父类)详解?

是类层次结构的根类,在java.lang.Object包里,每个类都是要它作为超类,所有对象(包括数组)都实现这个类的方法

  • Object类的equals()方法:指示其他某个对象是否与此对象“相等,只能比较引用类型。

    public boolean equals(Object obj) {
            return (this == obj);
    }
    //可见默认的Object的equals方法直接 用==判断,判断的是地址是否相等。子类往往需要重写
    

    可以判断基本数据类型,判断的是值是否相等,也可以判断引用数据类型,判断的是地址是否相等,即是否是一个对象

    子类没有重写 Object 类中的 equals 方法,equals方法和==号比较引用数据类型无区别,重写后的equals方法比较的是对象中的属性。

    String类的equals方法,重写了Object的equals方法,判断的是两个字符串的内容是否相等,每个字符逐一比较

    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String aString = (String)anObject;
                if (coder() == aString.coder()) {
                    return isLatin1() ? StringLatin1.equals(value, aString.value)   //拉丁语和UTF16分开处理
                                      : StringUTF16.equals(value, aString.value);
                }
            }
            return false;
    }
    
  • Object类的hashCode()方法:返回该对象的哈希码值。默认情况下,该方法会根据对象的地址来计算。(java是在虚拟机jvm跑的,所以一般没有运行地址一说,这里的地址只是参考概念)

    不同对象的 hashCode() 的值一般是不相同。但是,同一个对象的hashCode() 值肯定相同,两个引用指向一个对象值也相同。

  • Object类的toString()方法:返回该对象的字符串表示。包名.类名@hashCode 一般会重写为输出内容。可以使用快捷键创建重写。

    public String toString() {
            return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    //getClass().getName() 类的包名+类名 
    //toHexString(hashCode())  将对象的hashcode值转换成16进制字符串
    //重写toString方法
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender=" + gender +
                    '}';
        }
    

    当输出一个对象时 System.out.println(person);会默认调用 person.toString 方法

  • Object类的finalize()方法,finalize 是一个 protected 的方法。虽然不是 public 的,但是这个方法也同样能够被所有子类继承

    当对象被回收时,系统会自动调用该对象的finalize方法,子类可以重写该方法,做一些释放资源的操作

    什么时候被回收:当某个对象没有任何引用时,则jvm就会认为这个对象是个垃圾,垃圾回收器就会回收(销毁)对象,在销毁对象前,会调用该对象的finalize方法,程序员可以再该方法中写业务逻辑代码,比如释放资源,数据库连接,文件资源释放等。

    其基本原理是:如果垃圾回收器准备对某对象占用的内存资源进行回收,会先将该对象放入回收队列,处于回收队列中的对象会执行其finalize()方法,做一些清除前的工作,例如资源释放等;直至下一次垃圾回收动作发生时才会真正回收对象占用的内存空间
    但这并不意味着我们在编程时把应该finalize()方法作为类似于“析构函数”使用。因为该方法只会在垃圾回收时才会执行,而对象可能是不被垃圾回收的。

    jdk9之后以弃用该方法,因为finalize()方法不能保证执行。实际开发中也不会使用。转而使用System.gc()显式调用垃圾收集器。调用这个方法,就相当于通知 JVM,程序员希望能够进行垃圾回收。

断点调试

F7 --跳入方法内 F8 --逐行执行代码 F9 --resume执行到下一个断点 shift + F8 --跳出方法

设置填入源代码 settings->Build,Execution->Debugger->Data Views->Stepping->将java.*和javax.*前的对号取消掉

类与对象进阶

类变量(静态变量)

Static修饰的成员变量,被同一个类的所有对象共享,在类加载的时候就生成了

  • 内存分布:JDK8之前,类变量主要存储在方法区中,JDK8以后,在加载类信息的时候通过反馈机制会在堆中生成相应的class对象

  • 定义语法

    • 访问修饰符 static 数据类型 变量名[推荐]
    • static 访问修饰符 数据类型 变量名
  • 访问类变量

    • 类名.类变量名 [推荐]
    • 对象.类变量名 [不推荐],IDEA中无语法提示

    类变量的访问也需要遵守相应的访问权限,定义为private则不能访问

  • 类变量的生命周期随着类的创建而创建,随着类的消亡而销毁。与具体的对象无关。

类方法(静态方法)

  • 定义语法

    • 访问修饰符 static 返回数据类型 方法名(){} [推荐]
    • static 访问修饰符 返回数据类型 方法名(){}
  • 调用方法 --也需要满足相应的访问权限

    • 类名.类方法名 [推荐]
    • 对象名.类方法名
  • 什么时候需要用到类方法

    当方法内不涉及到任何与对象相关的成员时,可以把方法设置为类方法(静态方法),可以不创建实例调用方法,提高开发效率

    比如JAVA的Math类中很多方法都是静态方法,所以可以直接Math.sqrt(9)

  • 类方法只能访问类成员,不能使用this和super

  • 普通成员方法既可以访问非静态成员也可以访问静态成员

main方法

  • main方法形式 public static void main(String[] args) {}

  • main方法是JAVA虚拟机调用的,所以该方法的访问权限必须是public

  • JAVA虚拟机在调用方法的时候不需要创建对象,所以必须加上关键字 static

  • main方法接收String类型的数组参数,该数组中保存执行 java 命令时传递给所运行的类的参数

  • 如下代码编译时 java hello vicen 参数名2 参数名3 结果会输出 第1各参数=vicen, ... ...

    public class hello(){
        public static void main(String[] args) {
            for (int i = 0; i < args.length; i++) {
                System.out.println("第" + (i+1) "个参数=" + args[i]);
            }
        }
    }
    
  • 由于main方法也是静态方法,也不可以调用本类的非静态成员,要访问本类的非静态成员需要先创建对象再调用。?

代码块?

又称为初始化块,属于类中的成员,类似于方法,用{}包起来但与方法不同,没有方法名、参数、返回值,不用通过对象显示调用。而是加载类时或创建对象时隐式调用。 修饰符可选,要写的话也只能写static {}; --分号可以写也可以不写。

  • 好处

    • 相当于另一种形式的构造器(对构造器的补充),可以在调用任何形式的构造器时都执行代码块内的初始化操作。
    • 应用场景:如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码重用性
  • 类什么时候被加载?

    • 创建对象实例时(new)
    • 创建子类对象实例,父类也会被加载
    • 使用类的静态成员时(静态方法,静态属性),使用子类的静态成员父类也会被加载
    public class CodeBlockDetails {
        public static void main(String[] args) {
            System.out.println(B.bb);  //调用子类的静态成员属性时,父类的静态代码块也会执行
            System.out.println(A.aa);  //由于上面调用子类的时候已经调用了父类的静态代码块,这里A的静态代码块不会被调用
            A a = new A();
            B b = new B();
        }
    }
    class A{
        public static int aa = 10;
        //没有实例化对象,调用类的静态成员时以下非静态代码块不会被加载    只有使用new创建实例对象,以下代码块才会执行
        {
            System.out.println("A的非静态代码块调用--");
        }
        static {
            System.out.println("A的静态代码块被调用~~");
        }
    }
    class B extends A{
        public static int bb = 20;
        {
            System.out.println("B的非静态代码块调用--");
        }
        static {
            System.out.println("B的静态代码块被调用~~");
        }
    }
    
  • static 代码块只会被调用一次

  • 普通代码块与类的加载无关系,只有在用new实例化对象时普通代码块才会被调用,并且可以被调用多次(与构造器相似)

    public class CodeBlockDetails {
        public static void main(String[] args) {
            //System.out.println(B.bb);  //调用子类的静态成员属性时,父类的静态代码块也会执行
            //System.out.println(A.aa);
            A a = new A();
            B b = new B();
        }
    }
    class A{
        public static int aa = 10;
        //没有实例化对象,调用类的静态成员时一下非静态代码块不会被加载
        {
            System.out.println("A的非静态代码块调用--");
        }
        static {
            System.out.println("A的静态代码块被调用~~");
        }
    }
    class B extends A{
        public static int bb = 20;
        {
            System.out.println("B的非静态代码块调用--");
        }
        static {
            System.out.println("B的静态代码块被调用~~");
        }
    }
    //运行结果
    A的静态代码块被调用~~        //先加载类信息,后创建实例化对象  所以先调用静态代码块
    A的非静态代码块调用--
    B的静态代码块被调用~~      
    A的非静态代码块调用--        //实例化B的对象时,会加载A的类信息,但A的静态代码块已经加载过了,所以只会调用A的非静态代码块
    B的非静态代码块调用--
    //如果把上面的实例化对象的两条语句交换位置  
    B b = new B();
    A a = new A();
    //输出结果为
    A的静态代码块被调用~~        //加载B的类信息时先加载了A的类信息
    B的静态代码块被调用~~
    A的非静态代码块调用--        //创建B的实例化对象时右先加载了B的类信息,由于static代码块已调用,所以只调用A的非静态代码块
    B的非静态代码块调用--
    A的非静态代码块调用--
    
  • 创建一个对象时,在一个类调用顺序是

    • 调用静态代码块和静态属性初始化(静态代码块和静态属性初始化调用的优先级一样,如果有多个按定义的先后顺序调用)
    • 调用普通代码块和普通属性的初始化(普通代码块和普通属性优先级一样,如果有多个同样按定义的先后顺序调用)
    • 最后调用构造器(不准确,其实是执行构造器的输出语句,上面的第二步其实是在构造器内隐式调用的)
  • 构造器调用时隐藏了默认调用了自己的普通代码块和普通属性初始化 (public int n = getVal(); 会去调用getVal()方法)

    public class CodeBlockDetails02 {
        public static void main(String[] args) {
            BB bb = new BB();
        }
    }
    class AA{
        {
            System.out.println("AA的普通代码块调用");
        }
        public AA(){
            System.out.println("AA()的构造器调用");
        }
    }
    class BB extends AA{
        {
            System.out.println("BB的普通代码块调用");
        }
        public BB(){
            //这里隐含调用了BB的普通代码块,注意与非静态代码块不同,普通代码块在加载类信息时并不会被调用
            System.out.println("BB()的构造器调用");
        }
    }
    //结果
    AA的普通代码块调用            //实例化BB对象时先调用父类的构造器,父类的构造器又隐式调用了父类自己的普通代码块
    AA()的构造器调用             //调用完父类的普通代码块后才执行父类的构造器
    BB的普通代码块调用            //执行完父类的所有要执行的后再回到子类,执行顺序与父类相同
    BB()的构造器调用
    

单例设计模式

设计模式:

  • 静态方法和属性的经典使用
  • 是在大量的实践中总结和理论化后的优选的代码结构、编程风格以及解决问题的思考方式。就像是经典的棋谱,针对不同的棋局可以使用不同的棋谱

单例设计模式:

  • 采取一定的方法保证在整个的软件系统中对某个类只能存在一个一个对象实例,并且该类只提供一个取得该对象实例的方法

  • 单例模式有两种方式

    • 饿汉式:还没有用到对象的时候在类内已经把对象创建好了,可能造成资源的浪费
    • 懒汉式:在使用对象时才会创建实例,存在线程安全的问题
  • 饿汉式实现步骤

    • 构造器私有化 --防止用户直接new
    • 类的内部创建一个静态对象并初始化(为了在静态的公共方法中可以放回该对象,故将其创建为静态对象)
    • 向外暴露一个静态的公共方法。 getInstance (为了在不创建时就可以调用方法)
  • 懒汉式实现步骤

    • 构造器私有化 --防止用户直接new

    • 定义一个static静态属性对象,但不直接利用new对它进行初始化

    • 提供一个public的static方法,判断如果当前类内对象为null则返回一个new的对象

    • 注意:懒汉式会存在线程安全的问题,多个线程同时调用getInstance方法,有可能创建出多个对象

      class Cat{
          private String name;
          private static Cat cat;
          private Cat(String name) {
              this.name = name;
          }
          public static Cat getInstance() {
              if(cat == null) {  //如果还没有创建Cat对象再创建
                  cat = new Cat("小猫");
              }
              return cat;
          }
      

Final关键字

final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:

不希望类的某个属性被修改,也可以用final修饰该属性 public final double TAX_RATE = 0.1;

不希望某个局部变量被修改,也可以用final修饰,final int NUM = 1.0; (这时NUM可以成为局部常量)

  • 被final修饰的基本类型变量(四类八种) 不可变!

  • 被final修饰的 引用类型变量 地址不可变!,内容可变!

    final char[] c = {'j','a','v','a'};
    System.out.println(c);
    c[0] = 'h';    //这里是可以修改的
    System.out.println(c);
    
  • final修饰的属性又叫常量,一般用XX_XX命名,注意要大写

  • final修饰的属性在初始化时必须赋初值,可以在定义属性时,或者在代码块中,或者在构造器中

    class A{
        public final double TAX_RATE = 0.1;  //在定义时直接赋初值
        public final double TAX_RATE2;
        public final double TAX_RATE3;
        public A() {   //构造器中对常量赋初值
            TAX_RATE2 = 0.2;
        }
        {   //代码块中对常量赋初值
            TAX_RATE3 = 0.3;
        }
    }
    
  • final也可以修饰形参,被修饰后该形参不能被改变(相当于const)

  • 一般一个类如果已经是final类了,就不需要对它下面的方法定义为final方法了

  • final不能修饰构造器

  • final和static往往搭配使用,效率更高,用他们修饰的属性时不会导致类加载,底层编译器做了优化。顺序可以颠倒。

    public class FlnalExer {
        public static void main(String[] args) {
            System.out.println(B.num);
        }
    }
    class B{
        //final 和 static 往往搭配使用 效率更高,不会导致类加载,底层编译器做了优化。
        public final static int num = 1000;
        static {
            System.out.println("B的静态代码块被调用");
        }
    }
    //加上final后最终输出结果只有 1000
    //去掉final 输出会有 B的静态代码块被调用   1000
    
  • 包装类String,Double,Integer,Float,Boolean,Byte,Short,Character都是final类型的,不可以继承

抽象类

如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。

  • 当一个类中含有抽象(abstract)方法时,需要将这个类声明为抽象(abstract)类,类内方法不能有 {}
  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  • 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
  • abstract只能修饰类和方法,不能修饰属性和其他的东西
  • 抽象方法不能使用private,final和static来修饰,因为这些与重写相违背。static关键字和重写无关
  • 抽象类配合模板使用实现模板设计模式。见实例com.svicen.abstract_

接口

接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。

除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

接口与类相似点:
  • 接口和类的修饰符都只能是 public 或者 默认
  • 一个接口可以有多个方法。

  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。

  • 接口的字节码文件保存在 .class 结尾的文件中。

  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:
  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法。所有方法都是默认public修饰的
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。使用 implements 关键字
接口特性
  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
抽象类和接口的区别?
  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

:JDK 1.8 以后,接口里可以有静态方法和方法体了。?

接口使用
  • 接口内的方法(抽象方法,默认实现方法,静态方法)
public interface AInterface {
    //写属性
    public int n1 = 10;
    //写方法(抽象方法,默认实现方法,静态方法)
    //在接口中,抽象方法可以省略abstract关键字
    public void hi();
    //jdk8及之后版本,可以有默认实现方法,但需要用default关键字修饰
    default public void ok(){
        System.out.println("ok");
    }
    //jdk8之后,可以有静态方法,不需要使用default
    public static void run(){
        System.out.println("run");
    }
}
  • 示例
public class InterFaceDetail {
}
interface IA{
    void say();   //默认public
    void hi();
}
class Cat implements IA{   //  Ctrl + i 快速实现接口的方法
    @Override
    public void say() {
    }
    @Override
    public void hi() {
    }
}
  • 抽象类实现接口时,可以不实现接口的方法

  • 一个类可以实现多个接口,

    interface IB{}
    interface IC{}
    class Dog implements IB,IC{
    }
    
  • 接口不能继承其它的类,但是可以继承多个别的接口,使用extends而不是implements?

    interface ID extends IB,IC {}
    
  • 接口VS继承

    继承的价值主要在于:解决代码的复用性和可维护性

    接口的价值注意在于:设计好各种规范(方法),让其他类取实现这些方法,更加的灵活,可以视为对单继承的补充

    继承是满足 is a 的关系(猫是动物),而接口只需满足 like a 的关系 (猫像鱼一样游泳)?

    public class InterFaceDetail02 {
        public static void main(String[] args) {
            littlemonkey littlemonkey = new littlemonkey("悟空");
            littlemonkey.climbing();
            littlemonkey.swiming();
            littlemonkey.flying();
        }
    }
    class monkey{
        private String name;
        public monkey(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void climbing(){
            System.out.println("猴子" + name + "会爬树");
        }
    }
    interface Swimming{
        void swiming();
    }
    interface Flying{
        void flying();
    }
    class littlemonkey extends monkey implements Flying,Swimming{  //可以继承类的同时,实现多个接口的方法
        @Override
        public void swiming() {
            System.out.println("通过学习," + getName() + "可以像鱼儿一样游泳");
        }
        @Override
        public void flying() {
            System.out.println("通过学习," + getName() + "可以像鸟儿一样飞翔");
        }
        public littlemonkey(String name) {
            super(name);
        }
    }
    
接口多态特性

如下例,只要是实现了USBInterface接口方法的类都可以作为参数传递给USBinterface

public class InterfacePoly {
    public static void main(String[] args) {
        Camera camera = new Camera();
        Phone phone = new Phone();
        Computer computer = new Computer();
        computer.work(phone);    //这里由于work的参数类型为USBInterface,所以参数可以传进它的子类
        computer.work(camera);
    }
}
public class Computer {
    public void work(USBInterface usbInterface) {
        //通过接口,调用方法  体现多态性      固定接口的方法格式
        usbInterface.start();
        usbInterface.stop();
    }
}
public class Phone implements USBInterface{
    @Override
    public void start() {
        System.out.println("手机开始工作");     //对接口做具体的实现
    }
    @Override
    public void stop() {
        System.out.println("手机停止工作");
    }
}

再比如,接口类型可以指向所有实现了接口内方法的类的对象

interface IF{}
class Monster implements IF{}
class Car implements IF{}
public static void main(String[] args) {
    IF if01 = new Monster();  //创建接口类型的变量,指向Monster类型  相当于向上转型
    if01 = new Car();         //接口类型可以 指向实现了接口的类的对象实例
}

可以创建接口类型的数组,对于不同位置的元素放不同的实现接口的类

USBInterface usb[] = new USBInterface[2];
//多态数组
usb[0] = new Phone();
usb[1] = new Camera();
for (int i = 0; i < usb.length; i++) {
    usb[i].start();
    usb[i].stop();
    if (usb[i] instanceof Phone) {
        ((Phone) usb[i]).call();
    }
}
接口多态的传递?

如果IG继承了IH接口,而Cat类实现了IG接口,那么实际上相当于Cat类也实现了IH接口

内部类

  • 可以直接访问外部类的所有成员,包括私有成员
  • 外部类只能先创建内部类对象,然后通过该对象调用内部类方法
  • 外部其他类不能创建内部类对象
  • 如果外部类和局部内部类的成员重名,默认遵循就近原则,如果想访问外部类的成员,使用外部类名.this.成员名
局部内部类
  • 局部内部类是定义在外部类的局部位置,通常是在外部类的方法中。

  • 局部内部类不能加访问修饰符,因为他的地位就相当于局部变量。但可以加final,不被别的类继承

  • 局部内部类的作用域仅仅在定义它的方法或代码块中。

  • 外部类只能定义内部类的方法中创建内部类对象,然后通过该对象调用内部类方法

    public class LocalInnerClass {
        public static void main(String[] args) {
            Outer01 outer01 = new Outer01();
            outer01.out2();
        }
    }
    class Outer01{
        private int n1 = 100;
        private void out1(){}
        public void out2(){
            //内部类只可以加 final 修饰符
            final class Inner01{
                //内部类可以直接访问外部类的所有成员。包括私有的
                public void in1(){
                    System.out.println("内部类调用  n1=" + n1);
                }
            }
            Inner01 inn01 = new Inner01();
            inn01.in1();
        }
    }
    
  • 外部其他类不能创建内部类对象

  • 如果外部类和局部内部类的成员重名,默认遵循就近原则,如果想访问外部类的成员,使用外部类名.this.成员名

    public void out2(){
            //内部类只可以加 final 修饰符
            final class Inner01{
                private int n1 = 200;
                //内部类可以直接访问外部类的所有成员。包括私有的
                public void in1(){
                    System.out.println("内部类调用  n1=" + this.n1);
    //Outer01.this本质就是外部类的一个对象,谁调用了out2方法,Outer01.this就指向哪个对象
                    System.out.println("外部类调用  n1=" + Outer01.this.n1);
                }
            }
    }
    
匿名内部类?
  • 本质是内部类,类的名字由系统分配,名字是外部类的名字 + $1

  • 同时还是一个对象,系统实现类的定义和对接口的实现后,new了一个对象并把它的地址返回给了我们要接收的变量

  • 由于匿名类没有类名,那么除了定义它的地方,其他地方无法调用,匿名内部类使用一次后就不可以再使用了

  • 基于接口的匿名内部类如下,匿名内部类的声明是一个表达式,所以要以 ; 结尾

  • 直接 new IA{ 在这里实现接口里的方法 };

    public class AnnoymousInnerClass {
        public static void main(String[] args) {
            Outer02 outer02 = new Outer02();
            outer02.method();
        }
    }
    class Outer02{
        private int num = 10;
        public void method(){
            //基于接口的匿名内部类
            //1.传统方式,是创建一个类Tiger,实现该接口  并创建对象
            //2.需求是 Tiger 只调用一次,后面不再使用  如果每次都定义一个类会造成资源浪费
            //3.使用匿名内部类简化开发
            //本质是继承了IA接口,然后对它要使用的方法进行了具体的实现
            IA tiger = new IA(){
                @Override
                public void cry() {
                    System.out.println("老虎叫唤");
                }
            };   //注意这里要有分号
            //编译类型为 IA  运行类型为 匿名内部类,底层定义了这个类并直接创建了对象,把对象的this赋给了tiger
            /*底层
                class Outer02$1 implements IA{
                    public void cry() {
                    System.out.println("老虎叫唤");
                    }
                }
               */
            System.out.println("tiger的运行类型为" + tiger.getClass()); //运行类型为 Outer02$1
            tiger.cry();
        }
    }
    interface IA{
        public void cry();
    }
    
  • 基于类的匿名内部类 本质是继承了Father类,然后对它要使用的方法进行了重写

    class Outer02{
        private int num = 10;
        public void method(){
            //基于接口的匿名内部类
            //1.传统方式,是创建一个类Tiger,实现该接口  并创建对象
            //2.需求是 Tiger 只调用一次,后面不再使用  如果每次都定义一个类会造成资源浪费
            //3.使用匿名内部类简化开发
            IA tiger = new IA(){
                @Override
                public void cry() {
                    System.out.println("老虎叫唤");
                }
            };
            System.out.println("tiger的运行类型为" + tiger.getClass());
            tiger.cry();
            //基于类的匿名内部类,与接口的区别在于()内可以有参数
            //father的编译类型 Father   运行类型 为匿名内部类 Outer02$2
            Father father = new Father("jack") {
            };
            System.out.println("father的运行类型为" + father.getClass());
        }
    }
    class Father{
        public Father(String name) {  //构造器
        }
        public void test() {
        }
    }
    
  • 基于类的匿名内部类与普通的定义类区别

    //匿名内部类的实现关键在于 后面的 {};    本质是继承了Father类,然后对它要使用的方法进行了重写
    Father father1 = new Father("jack") {};  //这里可以什么都不做,即没有对Father类的方法进行重写
    System.out.println("father的运行类型为" + father1.getClass());  //运行类型为 匿名内部类  Outer02$2  按顺序编号
    //普通的new一个对象实例并赋地址给 father2  
    Father father2 = new Father("jack");
    System.out.println("father的运行类型为" + father2.getClass()); //运行类型为Father
    class Father{
        public Father(String name) {  //构造器
        }
        public void test() {
        }
    }
    
  • 基于抽象类的匿名内部类,与上类似,只不过{}内必须把抽象类的方法做出具体实现。

匿名内部类存在的前提是要有继承或者实现关系的,但是并没有看到extends和implements关键字,由底层JVM实现?

匿名内部类其实还是一个对象,系统实现类的定义和对接口的实现后,new了一个对象并把它的地址返回给了我们要接收的变量

如上基于接口的匿名内部类

new IA(){
    @Override
    public void cry() {
        System.out.println("老虎叫唤");
    }
}.cry();      //当做对象直接调用方法
  • 可以直接访问外部类的所有成员,包括私有成员

  • 不能添加访问修饰符,因为它的地位就是一个局部变量

  • 作用域仅仅在定义它的方法或者代码块中

  • 外部其他类不可以访问匿名内部类

  • 如果外部类和匿名内部类的成员重名,默认遵循就近原则,如果想访问外部类的成员,使用外部类名.this.成员名

    如果匿名内部类重新定义了它所继承的父类的属性(前提是父类的属性不为private)那么调用

class Outer02{
    private int num = 10;
    public void method(){  
        new Father("jack") {
            //private int num = 20;
            @Override
            public void test() {
                System.out.println("匿名内部类调用的num=" + num);
//首先看内部类有没有重新定义num,没有则取继承的父类即Father找是否有num且可以访问,还没有则找外部类的num
                //System.out.println("Father类的 num=" + Outer02.this.num); 这是调用外部类的num值
                //   Outer02.this 就是调用了method方法的对象
            }
        }.test();  //当成对象直接调用方法 
    }
}
class Father{
    protected int num = 30;
    public Father(String name) {  //构造器
    }
    public void test() {
    }
}
  • 最佳实践,匿名内部类可以直接当成参数使用
public static void main(String[] args) {
        //匿名内部类可以当做实参直接传递
        f1(new IG() {
            @Override
            public void show() {
                System.out.println("这是一幅名画");
            }
        });
    }
    //静态方法  为了直接在main里调用
    public static void f1(IG ig){
        ig.show();
    }
}
interface IG{
    void show();
}

实例二

public class InnerClassExercise02 {
    public static void main(String[] args) {
        //测试手机的闹钟功能,通过匿名内部类(对象)作为参数,打印懒猪起床了 和 小伙伴上课了
        CellPhone cellPhone = new CellPhone();
        //这里的函数参数是一个实现了Bell接口的匿名内部类  重写了ring方法
        cellPhone.alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("懒猪起床了");  //运行类型为 InnerClassExercise02$1
            }
        });
        cellPhone.alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("小伙伴上课了"); //运行类型为 InnerClassExercise02$2
            }
        });
    }
}
//铃声接口
interface Bell{
    void ring();
}
class CellPhone{
    //闹钟功能
    public void alarmclock(Bell bell) {
        //这里传入的bell 编译类型为Bell   运行类型为调用时创建的匿名内部类(是个对象)
        bell.ring();  //调用ring方法,首先去自己的运行类型里找,运行类型里重写了ring方法,所以调用自己重写后的ring
        System.out.println("运行类型为" + bell.getClass());  
    }
}
成员内部类

没有定义在外部类的方法中,而是定义在外部类的成员的位置上

public class MemberInnerClass {
    public static void main(String[] args) {
        Outer03 outer03 = new Outer03();
        outer03.T();
    }
}
class Outer03{
    private int n1 =10;
    public String name = "张三";
    //成员内部类,没有定义在外部类的方法中
    class Inner03{
        public void say(){
            //可以直接访问外部类的所有成员,包括私有成员
            System.out.println("name=" + name + "  n1=" + n1);
        }
    }
    //使用成员内部类
    public void T(){
        Inner03 inner03 = new Inner03();
        inner03.say();
    }
}
  • 可以直接访问外部类的所有成员,包括私有成员

  • 可以添加任意访问修饰符(public,private,protected,默认),因为它的地位是一个成员(这一点与局部内部类和匿名内部类不同)

  • 作用域为整个外部类(因为成员内部类就是外部类的一个成员),这一点与局部内部类和匿名内部类也不同

  • 外部类要访问成员内部类,必须先创建对象,再通过对象访问方法

  • 外部其他类也可以使用成员内部类(与上面两个也不同)

    • 外部其它类使用成员内部类的三种方式
    • 第一种
    Outer03 outer03 = new Outer03();
    Outer03.Inner03 inner031 = outer03.new Inner03()  //把内部类当成成员
    
    • 第二种

    在外部类中编写一个方法,返回创建好的成员内部类对象实例

    public class MemberInnerClass {
        public static void main(String[] args) {
            Outer03 outer03 = new Outer03();
            //Outer03.Inner03 inner031 = outer03.new Inner03();  方式1
            Outer03.Inner03 inner032 = outer03.getInner03Instance;  //方式2
        }
    }
    class Outer03{
        class Inner03{
            public void say(){
                //可以直接访问外部类的所有成员,包括私有成员
            }
        }
        public Inner03 getInner03Instance(){
            return new Inner03();
        }
    }
    
    • 第三种
    Outer03.Inner03 inner033 = new outer03().new Inner03()  //其实就是第一种两句合成了一句
    
  • 如果外部类和成员内部类的成员重名,默认遵循就近原则,如果想访问外部类的成员,使用 外部类名.this.成员名

静态内部类
public class StaticInnerClass {
    public static void main(String[] args) {
        Outer04 outer04 = new Outer04();
        outer04.m();
    }
}
class Outer04 {
    private int n1 = 10;
    public static String name = "张三";
    //同成员内部类,静态内部类的地位也是外部类的一个成员
    static class Inner04 {
        public void say(){
            System.out.println(name);
        }
    }
    //定义方法去创建内部类对象并调用内部类的方法
    public void m(){
        Inner04 inner04 = new Inner04();
        inner04.say();
    }
}
  • 放在外部类的成员的位置,但添加了static修饰符

  • 可以直接访问外部类的所有静态成员,包括私有的,但不能直接访问非静态成员

  • 可以添加任意的访问修饰符,因为它的地位就是一个成员。

  • 外部类要访问成员内部类,必须先创建对象,再通过对象访问方法

  • 作用域:为整个外部类体

  • 如果外部类和局部内部类的成员重名,默认遵循就近原则,如果想访问外部类的成员,使用外部类名.this.成员名

  • 外部其他类也可以使用成员内部类(直接通过内部类名 前提访问控制不为private)

    • 方式1,直接通过外部类名访问到内部类,前提要有访问权限
    //这里解释一下  首先是 Outer04.Inner04()作为一个整体   相当于new了一个Outer04.Inner04() 创建了一个内部类对象
    Outer04.Inner04 inner04 = new Outer04.Inner04();  //之前成员内部类是 先new了外部类在new了内部类
    inner04.say();
    
    • 方式2,编写一个方法,返回静态内部类的对象实例
    Outer04 outer04 = new Outer04();   //创建外部类对象实例
    Outer04.Inner04 inner041 = outer04.getInner04();  //利用外部类对象调用它的返回静态内部类的对象的方法
    inner041.say();
    public Inner04 getInner04(){
        return new Inner04();
    }
    
    • 方式3,编写一个静态方法(可以直接用外部类名 . 静态方法名调用),返回静态内部类的对象实例
    //以下方法更简洁,充分利用了静态内部类的静态特性,
    Outer04.Inner04 inner04_ = Outer04.getInner04_();
    inner04_.say();
    public static Inner04 getInner04_(){
        return new Inner04();
    }
    

枚举类

  • 枚举类的对象是确定的,只有有限个。例如,如果把季节定义成类,那么这个类只有四个对象:春夏秋冬。此时就能把季节定义为一个枚举类,这个枚举类的对象是确定的并且只有有限个。
  • 需要定义一组常量时,推荐使用枚举类。
  • 如果枚举类只有一个对象,则可以作为一种单例模式的实现方式。

自定义枚举类

  • 第一步,构造器私有化,防止直接new

  • 第二步,去掉set方法,防止属性被修改

  • 在类内部,直接创建固定对象

    public static final Season SPRINT = new Season("春天","温暖");
    public static final Season SUMMER = new Season("夏天","炎热");
    public static final Season AUTUMN = new Season("秋天","凉爽");
    public static final Season WINTER = new Season("冬天","寒冷");
    

使用enum关键字实现枚举类

  • 将类的定义 class 改为 enum
enum Season2{
    //使用enum关键字
    SPRINT("春天","温暖"),
    SUMMER("夏天","炎热");      //这里的两个参数对应构造器的两个参数
    private String name;
    private String desc; //描述
    private Season2(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }
    @Override
    public String toString() {
        return "Season2{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}
  • 如果有多个常量(对象),使用,间隔
  • 如果用enum实现枚举,要求将定义的常量对象写在最前面。位置固定,不然报错
  • 本质:定义 enum Season2 实际上是 Season2 继承了 java 的java.lang.Enum类,而且是一个final类,然后定义了对应的public static final常量
  • 由于enum已继承了Enum类,所以不能加extends关键字了,但可以继承接口,加implements

javap反编译

//命令行界面,使用javap 命令对生成的.class文件进行反编译
D:\JetBrains\IDEA\enum_annotation\out\production\enum_annotation\com\svicen\enum_>javap Season2.class
Compiled from "EnumExercise02.java"
final class com.svicen.enum_.Season2 extends java.lang.Enum<com.svicen.enum_.Season2> {
  public static final com.svicen.enum_.Season2 SPRINT;
  public static final com.svicen.enum_.Season2 SUMMER;
  public static com.svicen.enum_.Season2[] values();
  public static com.svicen.enum_.Season2 valueOf(java.lang.String);
  public java.lang.String getName();
  public java.lang.String getDesc();
  public java.lang.String toString();
  static {};
}
  • 如果使用无参构造器创建枚举对象,小括号也可以省略。WINTER即可

  • 如果enum 枚举类内没有重写 toString方法,则回去调用父类的toString方法,父类即Enum类,直接访问它定义的名字

    //Enum类的属性由 name  和 ordinal  构造器如下
    protected Enum(String name, int ordinal) {
            this.name = name;         //这里的name即为定义时的SPRING  SUMMER 等  连同索引作为参数传给了父类的构造器
            this.ordinal = ordinal;
    }
    public String toString() {
        return name;     //上面的Season如果将toString注释,输出Season2.SPRINT  即为 SPRING
    }
    

    Enum类的常用方法

方法名称 描述
values() 以数组形式返回枚举类型的所有成员 调用时用枚举类名.values()
valueOf() 将普通字符串转换为枚举实例,要求字符串为已有的常量
compareTo() 比较两个枚举成员在定义时的顺序(索引),大于返回1,等于返回0,小于返回-1
ordinal() 获取枚举对象的索引位置(从0开始)
name() 获取枚举对象的名字
toString() Enum类已经重写过了,返回的是当前对象的对象名,子类可以重写该方法
System.out.println("name=" + Season2.SPRINT.name());
System.out.println("ordinal=" + Season2.AUTYMN.ordinal());
Season2[] values = Season2.values();
for (Season2 season:values) {
    System.out.println(season);
}
Season2 summer = Season2.valueOf("SUMMER");
System.out.println("summer.ordinal=" + summer.ordinal());
System.out.println(summer.compareTo(Season2.SUMMER));  //结果为0  比较的就是ordinal

注解

Java 注解(Annotation)又称 Java 标注,Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

内置的注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。

作用在代码的注解是

  • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。

    @Target(ElementType.METHOD)                        //标记这个注解应该是哪种 Java 成员
    //标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。

  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。一直警告(黄色提示)

    @SuppressWarnings({"all"})       //注意它有作用范围,方法或类上面作用于方法或类内部
    //该注解类内部有一个属性String[] value(); 所以传的时候可以传多种警告,比如@SuppressWarnings({"all","unused"})
    //作用于@SuppressWarnings的注解
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
    

作用在其他注解的注解(或者说元注解)是:

  • @Retention-标识这个注解怎么保存,在编译代码中(RetentionPolicy.SOURCE),还是编入class文件中(RetentionPolicy.CLASS)(默认是这个),或者是在运行时可以通过反射访问(RetentionPolicy.RUNTIME)(运行时还保存,保留时间最长)。
  • @Documented - 标记这些注解是否包含在用户文档中。生成javaDOC时该注解会被保留在文档上
  • @Target - 标记这个注解可以修饰的 Java 成员
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

异常

执行过程发生的异常有两大类

  • Error,Java虚拟机无法解决的严重问题,如JVM系统内部错误,资源耗尽等严重情况。比如StackOverflowError和OOM(out of memeory),会导致程序的崩溃
  • Exception:其它因为编译错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码对其进行处理。例如空指针访问,识图读取不存在的文件,网络连接中断等等。Execption分为两大类,运行时异常和编译时异常。
    • 运行时异常:RunTimeException:有NullPointerException,ArithmeticException(分母为0),有默认处理机制 ArrayIndexOutOfBoundsException,ClassCastException(类型转换),NumberFormatException(数字格式异常)等
    • 编译时异常:FileNotException:ClassNotFoundException(加载不存在的类),必须处理

进入Throwable后查看源码,右键Diagrams -> show Diagrams

image-20220421104141869

异常处理方式

  • try-catch-finally Ctrl+alt +t 选择异常

    int num1 = 1,num2 = 0;
    try {
        int num3 = num1 / num2;  //可能有异常的代码块
    } catch (Exception e) {
        //当异常发生时,系统将异常封装到Exception对象e,传递给catch,程序员可以自己处理
        //没有发生异常,catch代码块是不会执行的
        //throw new RuntimeException(e);
        System.out.println("异常为" + e.getMessage());
    } finally {
        //不管try代码块有没有发生异常,始终要执行finally代码块,
        //通常将释放资源的代码放在这里
    }
    
  • throws

    将发生的异常抛出,交由调用者处理,最终JVM调用了main函数,每一步都有两种处理方式。默认throws(二选一即可)

    image-20220421110240880

try-catch细节

  • 如果发生异常,则异常发生后面的代码不会执行,直接进入到catch代码块
try {
    String str = "异常";
    int a = Integer.parseInt(str);
    System.out.println("转换后:" + a);  //不会执行
} catch(NumberFormatException e) {
    System.out.println("异常信息:" + e.getMessage());
} finally{
    System.out.println("finally代码块执行");
}
System.out.println("程序继续执行");  //会执行
  • 如果异常没有发生,则顺序执行try的代码块,不会进入到catch

  • 如果希望无论是否发生异常都执行某段代码(比如断开连接、释放资源),可以加上finally

  • try代码块可以有多个异常,可以有多个catch语句,分别捕获不同异常,要求子类异常要写在前面,父类异常(Exception e)写在后面

  • 可以进行try-finally配合使用,应用于:执行一段代码,不管是否发生异常,都必须执行某个业务逻辑

    相当于没有捕获异常,执行完finally的语句后程序会直接崩掉/退出

    try {
        int n1 = 10;
        int n2 = 0;
        System.out.println(n1 / n2);
    }finally {
        System.out.println("finally语句块执行...");
    }
    System.out.println("程序继续执行");  //这条语句不会执行
    //如下返回结果为4,即使捕获到了NullPointerException异常,但是catch下的return语句还是没有执行,执行了finally语句
    //注意 catch下的return不会执行,但是 相关的语句还是会执行的 比如 return ++i;  i还是会完成自增,但不会返回
    

    image-20220421112403640

注意:下面的catch语句块中会将++i后的值作为临时变量先保存起来再去执行finally语句,最后返回这个临时变量

image-20220421112848951

throws细节

  • 对于编译异常,程序中必须处理。 快捷键 : alt + 回车
  • 对于运行时异常,程序中如果没有处理,默认是throws
  • 如果一个方法可能生成某种异常,但是并不能确定如何处理这些异常,则此方法应显示地声明抛出异常,表名该方法将不对异常进行处理,而由该方法的调用者负责处理。
  • 在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的具体异常类型,也可以是它的父类,也可以是一个异常列表,即可以抛出多个异常,如下所示
public class Throws01 {
    public static void main(String[] args) {
    }
    public void f1() throws FileNotFoundException,NullPointerException,ArithmeticException  {
        //创建了一个文件流对象,
        //这里的异常是一个FileNotFoundException,编译时异常
        //可以使用try-catch-finally
        //使用throws,抛出异常  throws FileNotFoundException  也可以是 throws Exception
        FileInputStream fileInputStream = new FileInputStream("d//aa.txt"); //编译异常
    }
}
  • 子类重写父类方法时,对抛出的异常的规定:子类重写的方法所抛出的异常要么和父类抛出的异常一致,要么为父类抛出异常的子类型,范围不能大于父类的异常范围。比如说父类抛出RunTimeException,子类抛出Exception是不允许的。
  • 如果f3调用f1,f1里有编译异常并抛出给调用者,f3里必须对异常进行处理,或者try-catch或者throws,但是如果f1有运行异常则f3可以不处理,默认是throws

自定义异常

一般是继承RunTimeException

public class Throws01 {
    public static void main(String[] args) {
        int age = 200;
        if(!(age >=18 && age <=120)) {
            throw new AgeException("年龄需在18-120");  //抛出异常,输出的即为我们这里传的参数
        }
        System.out.println("你的年龄范围正确");
    }
}
class AgeException extends RuntimeException{
    public AgeException(String message) {  //构造器
        super(message);
    }
}
意义 位置 后面跟的东西
throw 手动生成异常对象的关键字,与new并用 方法体中 异常对象
throws 异常处理的一种方式 方法声明处 异常类型

包装类

所有的包装类(Integer、Long、Byte、Double、Float、Short)都是抽象类 Number 的子类。

这种由编译器特别支持的包装称为装箱,所以当内置数据类型被当作对象使用的时候,编译器会把内置类型装箱为包装类。相似的,编译器也可以把一个对象拆箱为内置类型。Number 类属于 java.lang 包。

//手动装箱   jdk5之前手动装箱和拆箱
int n1 = 10;
Integer integer = new Integer(n1);   // jdk9以后 @Deprecated方法过时
//手动拆箱
int i = integer.intValue();
//自动装箱  jdk5之后可以
int n2 = 20;
Integer integer2 = n2;
System.out.println(n2);
//自动拆箱
int n3 = integer2;
System.out.println(n3);
//注意看以下代码  三元运算符是一个整体,虽然执行new Integer(1)但整体精度为double,输出1.0
Object obj1 = true ? new Integer(1):new Double(2.0);
System.out.println(obj1);  // 1.0

包装类方法

  • 包装类转字符串

    Integer i = 100;
    //方式1
    String str1 = i + "";
    //方式2
    String str2 = i.toString();
    //方式3
    String str3 = String.valueOf(i);
    
  • 字符串转包装类

    //String->Wrapper
    String str4 = "123";
    Integer i2 = Integer.parseInt(str4);
    //方式2
    Integer integer = new Integer(str4);
    

经典题目

Integer m = 1;
Integer n = 1;
System.out.println(m == n);   //true  这里没有new对象
Integer x = 128;
Integer y = 128;
System.out.println(x == y);      //false 这里返回的是new的对象
int n = 10;
Integer nn = 10;
System.out.println(n == nn);  //false 只要有基本数据类型判断的就是值是否相等
//Integer m = 1; 底层是调用了Integer的 valueOf方法,
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)  //low为-128   high为127 Integer的缓存机制
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

String类

image-20220422142032445

  • String类实现了Comparable(对象可以比较)和Serializable接口(说明String可以串行化,可以在网络传输)

String的字符使用Unicode字符编码,一个字符(不区分字母还是汉字)占两个字节

String类有很多构造器的重载,常用的有

String s1 = new String();
String s2 = new String(String original);
String s3 = new String(char[] a);
String s4 = new String(char[] a,int startIndex,int count);
String s5 = new String(byte[] b);
  • String是final类,不能被其他的类继承,String有属性,private final byte[] value; 用于存放字符串内容,value数组也有final关键字,不可以修改(指的是value[]不可以指向新的地址,但具体的字符是可以改的)。

对象的创建

//1、直接在常量区找是否有"svicen"的数据空间,有则将s1指向该空间,没有则重新创建,然后指向。s1最终指向的常量池的空间地址
String s1 = "svicen";
//2、现在堆中创建空间,里面维护了value属性指向常量池的"svicen"的数据空间,如果常量池没有则创建后指向。s2指向的是堆的空间地址
String s2 = new String("svicen");

String的intern方法,如果池已经包含一个等于此String对象的字符串(用equals(Object)判断),则返回池中的字符串。否则,将此String对象添加到池中,并返回此对象的引用。 最终返回的是常量池的地址

经典题目

Person p1 = new Person();   //在堆区创建了两个Person对象,他的name属性时字符串,字符串的value指向常量区的"svicen"
p1.name = "svicen";
Person p2 = new Person();
p2.name = "svicen";
System.out.println(p1.name.equals(p2.name)); //true
System.out.println(p1.name == p2.name);     //true  只要常量区里有"svicen"就直接指向,所以p1 p2的value指向的地址相同
System.out.println(p1.name == "svicen");    //true  p1.name的value[]指向的就是常量区的"svicen"的地址
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2);               //false  这里新创建了两个对象,对象的地址一定是不一样的
System.out.println(s1.intern() == s2.intern()); //true intern方法返回了两个对象的value指向的常量区的地址
//关于字符串对象创建的问题
String s1 = "hello";
s1 = "haha";              //创建了两个对象  因为字符串的值不能改变,修改后相当于新创建了一个对象让s1指向它
String a = "hello" + "abc";  //创建一个对象,编译器会做优化,判断创建的常量区对象是否有引用指向,
//等价于 String a = "helloabc"
String a = "hello";
String b = "abc";
String c = a + b;       //创建三个对象                             // d = "helloabc"
//1.先创建一个StringBuilder sb = StringBuilder()
//2.执行sb.append("hello");
//3.执行sb.append("abc");
//4.执行sb.toString()方法,return 上面的字符串  返回给c
//最后实际上是 c 指向堆中的对象(String) value[] value数组再指向 常量池的"helloabc"  所以 c==d 返回false
// 字符串常量相加结果直接指向常量池   字符串变量相加结果指向堆的内存

String常用方法

int length():返回字符串的长度:return value.length
char charAt(int index):返回某索引处的字符  return value[index]   不要使用str[0]取第一个字符
boolean isEmpty():判断是否是空字符串:return value.length == 07
String toLowerCase():使用默认语言环境,将String 中的所有字符转为小写
String toUpperCase():使用默认语言环境,将String 中的所有字符转为大写
char[] toCharArray():将String转换成一个char数组 
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String str) 功能与equals相似,忽略大小写
String concat(String str):将指定字符串连接到此字符串的结尾,等价于”+“
int compareTo(String str):比较两个字符串的大小,前者大返回正数,后者大返回负数,长度和内容都相同返回0
String a = "jacks";
String b = "jaaks";
System.out.println(a.compareTo(b));  //每一位依次比较,最终返回 'c' - 'a' = 2
a = "jaak";
System.out.println(a.compareTo(b)); //如果二者已比较的的都相等,但长度不等,最后返回的是二者的长度的差值
String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex位置开始截取
String substring(int beginIndex,int endIndex):返回一个新的字符串,它是此字符串的从beginIndex位置开始截取到endIndex位置,
boolean endsWidth(String str):测试此字符串是否以指定的后缀结束
boolean startsWidth(String str):测试此字符串是否以指定的前缀开始
boolean startsWidth(String str, int toffset):测试此字符串是否在指定索引开始的位置以指定的前缀开始  
boolean contains(CharSequence s):当且仅当此字符串包含指定的char值序列时返回true
int indexOf(String str):返回指定子字符串在此字符串中第一次出现的索引,找不到返回-1
int indexOf(String str,int index):返回指定子字符串在此字符串中第一次出现的索引,从指定索引处开始查找
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现的索引,找不到返回-1
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现的索引,从指定索引处开始查找
String replace(char oldChar,char newChar):返回一个新的字符串,它是通过newChar替换此字符串中出现的所有oldChar得到的
s2 = s1.replace("a","b");  //对于s1本身没有影响
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):根据给定正则表达式的匹配拆分此字符串,s = s.split(",");以逗号为分割点分割
String[] split(String regex,int limit):根据匹配的正则表达式来拆分此字符串,最多不超过limit个,如果超过,剩下的全部放到最后一个元素中。
String format(String format, Object args):就是利用%S %d %x占位符   

JAVA的格式化字符串 format

String formatStr = "我的名字是%s,年龄是%d,成绩是%.2f,性别是%c.";
String info = String.format(formatStr,a,age,score,sex);
System.out.println(info);

由于String每次更新对象都需要重新开辟空间,效率较低,因此有StringBuilder和StringBuffer来增强String的功能

StringBuffer

  • StringBuffer的直接父类是AbstractStringBuilder,StringBuffer也实现了Serializable接口,即它创建的对象也可以串行化

  • 在父类AbstractStringBuilder中有属性char[] value,不是final修饰的,所以该value数组存放在堆里,不再是常量池

  • StringBuffer也是一个final类,不可以被继承

  • StringBuffer保存的是字符串变量,里面的值可以修改,每次StringBuffer的更新实际上可以更新内容,不用每次更新地址

    即创建新对象),所以效率要比String高

StringBuffer方法

  • 构造器

    //创建一个大小为16的char数组
    StringBuffer stringBuffer = new StringBuffer();
    //通过构造器指定char value数组的大小
    StringBuffer stringBuffer1 = new StringBuffer(100);
    //通过构造器直接赋值一个字符串,char数组大小为16+length(传入的字符串)
    StringBuffer hello = new StringBuffer("hello");
    
  • String与StringBuffer的转换

    //String->StringBuffer
    //方法一:使用构造器,对str本身没有影响,只是返回的是一个StringBuffer对象
    String str = "svicen";
    StringBuffer stringBuffer2 = new StringBuffer(str);
    //方法二:使用append方法,同样对str本身没有影响,只是拷贝了一份str追加到了当前StringBuffer对象的末尾
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str);
    //StringBuffer->String
    //方法一:使用StringBuffer的toString方法
    StringBuffer hello = new StringBuffer("hello");
    String s = hello.toString();
    //方法二:使用构造器
    String s1 = new String(stringBuffer2);
    
  • 1、append方法

    public StringBuffer append(boolean b)

    该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也发生 改变。

  • 2、insert方法

    public StringBuffer insert(int offset, boolean b)

    作用是在StringBuffer对象中插入内容,然后形成新的字符串。

  • 3、delete()方法

    s.delete(1,2); 删除1-2的字符,左闭右开

  • 4、replace()方法

    s.replace(1,2,'a'); 把索引为1的字符替换为 'a'

  • 5、deleteCharAt方法

    public StringBuffer deleteCharAt(int index)

    该方法的作用是删除指定位置的字符,然后将剩余的内容形成新的字符串。

  • 6、reverse方法

    public StringBuffer reverse()

    作用是反转StringBuffer对象中的内容,然后形成新的字符串。

  • 7、trimToSize方法

    public void trimToSize()

    该方法的作用是将StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费。

  • 8、setCharAt方法

    public void setCharAt(int index, char ch)

    作用是修改对象中索引值为index位置的字符为新的字符ch。

题目实例,对金额形式的转换,每三位添加一个,

Scanner scanner = new Scanner(System.in);
String str = scanner.next();
StringBuffer stringBuffer = new StringBuffer(str);
for (int i = stringBuffer.lastIndexOf(".") - 3; i > 0; i -= 3) {
    //123456.59
    //先找到小数点位置
    stringBuffer.insert(i,",");
}
System.out.println(stringBuffer);

StringBuilder

一个可变的字符序列,此类提供一个与StringBuffer兼容的API,但不保证同步(会有线程安全问题),此类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候,如果可能,建议优先采用该类,因为大多数实现中,它比StringBuffer更快

StringBuilder上的主要操作是append和insert方法,可重载这些方法,以接受任意类型的数据。

  • StringBuilder的直接父类是AbstractStringBuilder,StringBuilder也实现了Serializable接口,即它创建的对象也可以串行化
  • 仍是final类,对象字符序列仍是存放在父类AbstractStringBuilder的属性char[] value中,
  • StringBuilder的方法,没有做互斥的处理,即没有synchronized关键字,因此在单线程的情况下使用

Math类

  • Math.sqrt() : 计算平方根

  • Math.cbrt() : 计算立方根

  • Math.pow(a, b) : 计算a的b次方

  • Math.max( , ) : 计算最大值

  • Math.min( , ) : 计算最小值

  • Math.abs() : 取绝对值

  • Math.ceil(): 天花板的意思,就是逢余进一

  • Math.floor() : 地板的意思,就是逢余舍一

  • Math.rint(): 四舍五入,返回double值。注意.5的时候会取偶数

  • Math.round(): 四舍五入,float时返回int值,double时返回long值

  • Math.random(): 取得一个[0, 1)范围内的随机数

Arrays类

  • Arrays.fill(Object[ ] array, Object obj):用指定元素填充整个数组(替换数组原元素)

  • Arrays.sort(Object [ ]arr, new Comparator() ):对传入数组进行递增排序,字符则按照ASCII进行排序(不区分大小写),可以自己指定排序方法,传入一个接口Comparator实现定制排序(重写compare方法)。sort底层调用了匿名内部类Comparator的compare方法

    Integer[] arr = {1,5,9,3,6};
    Arrays.sort(arr);   //默认升序 
    System.out.println(Arrays.toString(arr));  //1,3,5,6,9
    Arrays.sort(arr, new Comparator<Integer>() {    //接口编程
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;            //降序排列
        }
    });
    System.out.println(Arrays.toString(arr)); //9,6,5,3,1
    
    //源码阅读
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {  //首先判断匿名内部类是否为空
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0); //底层调用的Timsort的sort方法,底层二分排序
        }
    }
    //binarySort的部分代码如下              
    while (left < right) {
        int mid = (left + right) >>> 1;  //无符号右移
        //体现动态绑定
        if (c.compare(pivot, a[mid]) < 0)//调用了我们实现的匿名内部类的compare方法  因此可以通过重载compare实现定制排序
            right = mid;
        else
            left = mid + 1;
    }
    
  • Arrays.equal(Object []arr,Object []nums):判断两个数组是否相等,实际上比较的是两个数组的哈希值,

  • Arrays.equals(Object []arr,Object []nums):判断两个数组的元素是否完全相同

  • Array.hashCode(Object []arr):返回数组的哈希值

  • Arrays.copyOf(Object [], int length):拷贝数组,其内部调用了 System.arraycopy() 方法,从下标0开始,拷贝指定长度的元素(length可选),如果超过原数组长度,会用null进行填充。

  • Arrays.copyOfRange(T[] original, int from, int to):拷贝数组,指定起始位置和结束位置,如果超过原数组长度,会用null进行填充

  • Arrays.toString(Object []arr):将数组中的内容全部打印出来

    Integer[] integer = {1, 30, 15};
    System.out.println(Arrays.toString(integer));
    //结果
    [1,30,15]
    
  • Arrays.binarySearch(Object []arr,T ans)二分查找法找指定元素的索引值(数组一定是排好序的,否则会出错。找到元素,只会返回最后一个位置) 如果不存在, - (low + 1) low 为按升序排该数字应该在该数组的位置。

  • Arrays.asList(arr):将arr里的数据转为List集合再返回。

System类

  • exit 退出当前程序,exit(0)表示异常退出
  • arrcopy(arr,0,dest,0,len):复制数组元素 第二个参数表示从原数组的哪个位置开始拷贝,第四个参数为目标数组的开始索引,第四个参数为拷贝长度
  • currentTimeMillis(),返回当前时间距离1970.1.1的毫秒数

大数处理

BigInteger

BigInteger bigInteger = new BigInteger("22222222222222222222222222222");
BigInteger bigInteger1 = new BigInteger("22222222222222222222222222222");
System.out.println(bigInteger);
//在对BigInteger进行加减乘除时需要使用对应的方法   将数字当成字符串,处理完后再转为数字
BigInteger sum = bigInteger.add(bigInteger1);
System.out.println(sum);
BigInteger sub = bigInteger.subtract(bigInteger1);
System.out.println(sub);
BigInteger multy = bigInteger.multiply(bigInteger1);
System.out.println(multy);
BigInteger div = bigInteger.divide(bigInteger1);
System.out.println(div);

BigDecimal

BigDecimal bigDecimal1 = new BigDecimal("1.11111111111111111111111111111111111111");
BigDecimal bigDecimal2 = new BigDecimal("1.11111111111111111111111111111111111111");
//也需要使用对应的方法对BigDecimal进行加减乘除
BigDecimal sum = bigDecimal1.add(bigDecimal2);
System.out.println(sum);
//对于除法  如果除不尽,有可能会抛出异常  -- 可以在divide方法后指定精度
BigDecimal div = bigDecimal1.divide(bigDecimal2);
BigDecimal div = bigDecimal1.divide(bigDecimal2,BigDecimal.ROUND_CEILING); 
//BigDecimal.ROUND_CEILING 在JDK9后已过时
System.out.println(div);

日期类

第一代日期类Date

  • Date:精确到毫秒,代表特定的时间 System.out.println(new Date());

    Date(123),传入的参数为毫秒,把毫秒转换为对应的时间

  • SimpleDateFormat:格式化和解析日期的类,允许进行格式化(日期->文本),解析(文本->日期)和规范化

    Date date = new Date();
    System.out.println(date);
    //Mon Apr 25 18:57:05 CST 2022
    //这里注意月份的占位符是MM,E是周几
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");
    String format = simpleDateFormat.format(date);
    System.out.println(format);
    //2022年04月25日 06:57:05 周一
    

第二代日期类Calendar

Calendar是抽象类,所以不可以实例化对象

缺点:可变性(日期和时间这样的类应该是不可变的),偏移性(月份从0开始),不能格式化,不是线程安全的,不能处理闰秒

Calendar c = Calendar.getInstance();
System.out.println("c=" + c);   //会将整个日历的字段全部打印出来
//如果要分别打印字段  需要自己组合,没有提供格式化字符串的方法
System.out.println("年:" + c.get(Calendar.YEAR));
System.out.println("月:" + (c.get(Calendar.MONTH) + 1));  //月份按照0开始编号的,所以需要+1
System.out.println("日:" + c.get(Calendar.DAY_OF_MONTH));
System.out.println("时:" + c.get(Calendar.HOUR));
System.out.println("时:" + c.get(Calendar.HOUR_OF_DAY)); //以24小时的方式获取 时
System.out.println("分:" + c.get(Calendar.MINUTE));
System.out.println("秒:" + c.get(Calendar.SECOND));

第三代日期类LocalDateTime

  • LocalDate(日期,年/月/日)
  • LocalDate(时间,时/分/秒)
  • LocalDateTime(年/月/日/时/分/秒) JDK8 加入
LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);
//2022-04-25T19:34:02.573627400
System.out.println("年:" + ldt.getYear());
System.out.println("月:" + ldt.getMonth());             //APRIL
System.out.println("月:" + ldt.getMonthValue());        //4
System.out.println("日:" + ldt.getDayOfMonth());
//使用DateTimeFormatter 对象对日期进行格式化
DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");//注意小时使用 HH 按24小时来算
String format = date.format(ldt);  
System.out.println("格式化日期:" + format);  //格式化日期:2022年04月25日 19:42:23
  • Instant时间戳:提供了一系列与Date类相互转换的方法
//通过静态方法,now 获取当前时间戳的对象
Instant now = Instant.now();
System.out.println(now);
//通过 Date类的from 方法可以把Instant转为Date
Date date = Date.from(now);
System.out.println(date);
//通过 date对象的toInstant方法可以把一个Date转换为Instant
Instant instant = date.toInstant();
System.out.println(instant);
//最终输出结果为
2022-04-25T11:47:50.124618200Z
Mon Apr 25 19:47:50 CST 2022
2022-04-25T11:47:50.124Z      可知再转换为Instannt对象后时间戳的精度比now出来的小
  • 对日期的加减
//对日期的加减
LocalDateTime ldt = LocalDateTime.now();
LocalDateTime localDateTime = ldt.plusDays(23);
System.out.println("23天后:" + localDateTime);
//也可以格式化输出
DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
System.out.println("23天后:" + date.format(localDateTime)); //23天后:2022年05月18日 19:55:16
//对日期的减
LocalDateTime localDateTime1 = ldt.minusHours(24);
System.out.println(date.format(localDateTime1));     //    减去24小时后:2022年04月24日 19:55:16

集合

ctrl + alt +b 在类图中查看子接口 alt + insert 快速插入方法

image-20220427154620682
image-20220427155432910

集合主要分为两类,单列集合和双列集合List和Set都是单列集合,map是双列集合。

List

ArrayList

底层维护了一个Object类型的数组transient Object[] elementData,transient 表示瞬间的,短暂的,表示该属性不会被序列化

List list = new ArrayList();
//add方法插入
list.add("svicen");
list.add(10);
list.add(true);
System.out.println(list);
//remove方法删除元素
list.remove("svicen");
System.out.println(list);
//contains方法查看是否有指定元素
System.out.println(list.contains(true));
//isEmpty方法判断是否为空
System.out.println(list.isEmpty());
//clear方法清空
list.clear();
//addAll方法,添加一个集合
ArrayList list1 = new ArrayList();
list1.add("西游记");
list1.add("红楼梦");
list.addAll(list1);
System.out.println(list);
//containsAll方法查找集合是否存在
System.out.println(list.containsAll(list1));
//removeAll方法删除集合
System.out.println(list.removeAll(list1));
  • 迭代器Iterator遍历
ArrayList col = new ArrayList();
col.add(new Book("三国演义","罗贯中",50));
col.add(new Book("西游记","吴承恩",40));
col.add(new Book("红楼梦","曹雪芹",60));
//使用迭代器遍历
//首先需要获得迭代器
Iterator iterator = col.iterator();
//快捷键 itit    查看所有快捷键 ctrl + j
while (iterator.hasNext()) {
    Object next =  iterator.next();
    System.out.println(next);
}
//退出循环后,iterator指向最后的元素
//如果需要再次遍历,需要重置迭代器
iterator = col.iterator();
  • 增强for循环遍历
//快捷键 I
for (Object book : col) {
    System.out.println(book);
}
  • list内存的元素是有序的(添加顺序和取出顺序一致)

  • list内可以有重复的元素

  • list的每个元素都有对应的顺序索引

    list.add("张三");
    list.add("李四");
    //在指定位置查出元素
    //在index=1的位置插入一个对象
    list.add(1,"刘备");
    System.out.println(list);
    ArrayList list2 = new ArrayList();
    list2.add("sv");
    list2.add("sva");
    //在index=1的位置插入一个集合
    list.addAll(1,list2);
    //删除index=1的元素
    list.remove(1);
    System.out.println(list);
    //返回元素"刘备"最后出现位置的索引
    System.out.println(list.lastIndexOf("刘备"));
    //在指定位置为元素赋值,相当于替换
    list.set(1,"svicen");
    //返回指定索引范围的集合 [1,3)
    List res = list.subList(1,3);
    
  • ArrayList的元素的值可以为null,ArrayList底层是用数组来实现数据存储的,它基本等同于Vector,除了ArrayList是线程不安全的(效率高),在多线程情况下,不建议使用ArrayLIst

  • ArrayList底层维护了一个Object类型的数组elementData[],如果使用无参构造器创建ArrayList对象,初始elementData容量为0,第一次添加,其容量扩充为10,如需要再次扩容,则扩容为原来的1.5倍

  • 如果使用有参构造器,则初始容量为指定大小,此后再次扩容则扩大为1.5倍

  • 源码剖析

    //使用无参构造器创建对象时,DEFAULTCAPACITY_EMPTY_ELEMENTDATA为一个空数组,并不给定初始大小
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //我们在代码中调用ArrayList的add方法,其实调用的是这个,然后这个add方法又调用了另一个重载后的add方法
    public boolean add(E e) {
        modCount++;     //modCount记录了数组的修改次数
        add(e, elementData, size); 
        return true;
    }
    //核心逻辑,判断是否需要扩容的逻辑,不需要便直接赋值并让size++
    private void add(E e, Object[] elementData, int s) {
        //如果s(即传进来的size)与当前数组的length相等,则说明再添加的话需要扩容
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    //这里的无参函数grow又调用了重载后的grow函数
    private Object[] grow() {
        return grow(size + 1);
    }
    //数组扩容,其实还是调用了数组的copyof方法
    private Object[] grow(int minCapacity) {
        //这里的newCapacity决定了扩容后的大小
        return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity)); 
    }
    //决定扩容是1.5倍还是10    比如说刚开始传入的minCapacity=1,然后下一次扩容传入11
    private int newCapacity(int minCapacity) { 
        // overflow-conscious code
        int oldCapacity = elementData.length; //第一次:0         第二次:10
        int newCapacity = oldCapacity + (oldCapacity >> 1); //第一次:0     第二次:15
        if (newCapacity - minCapacity <= 0) {  //第一次:0-1<0       第二次:15-11>0
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) //此时elementData还等于默认的空集合
                return Math.max(DEFAULT_CAPACITY, minCapacity);//DEFAULT_CAPACITY为10
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8    
        //如果要扩容的大小超过了最大限制,调用hugeCapacity进行扩容,否则直接返回newCapacity即可
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
    }
    //这里newLength即为10
    public static <T> T[] copyOf(T[] original, int newLength) { 
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    //
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //这里已经将copy数组的长度设置为了10
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]  
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        //关键一步, 参数依次为原数组,srcPos,拷贝后的数组,srcPos,拷贝的长度
        //拷贝长度Math.min(original.length, newLength),为了防止原数组很长,每次只拷贝一部分
        System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
        return copy;
    }
    
Vector
  • Vector底层也是一个对象数组,protected Object[] elementData

  • Vector是线程同步的,即线程安全的,Vector类的方法带有synchronized,但效率不如ArrayList

  • 使用无参构造器创建Vector对象时,直接会给定初始数组大小为10,这里与ArrayList不同,之后每次扩容2倍

  • 源码剖析

    //自己写的代码
    Vector vector = new Vector();
    for (int i = 0; i < 10; i++) {
        vector.add(i);
    }
    //追进去的源码
    //调用无参构造器时,直接给了初始容量10
    public Vector() {
        this(10);
    }
    //之后进行add方法的操作,与上面几乎完全一样,唯一不同的函数为newCapacity,即确定扩容后大小的函数
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  //10
        //capacityIncrement为vector方法的属性,为了提供一个api让用户自己指定扩容的增量
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ? //0>0不成立,newCapacity = 2*oldCapacity
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
    }
    
LinkedList
  • 双向链表,存放了first和last两个结点,每个节点有prev,next,item三个属性,也是线程不安全的。
  • 源码剖析
//自己写的代码
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
//默认删除第一个结点
linkedList.remove();
//跟进去的源码
public boolean add(E e) {
    linkLast(e);
    return true;
}
//默认向链表的最后添加结点
void linkLast(E e) {
    final Node<E> l = last;
    //新结点的prev指向l,值item为e,next指向null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //如果l为空,说明原来链表里没有元素,直接让first也指向新添加的结点即可
    if (l == null)
        first = newNode;
    else//l不为空,则让l.next指向新添加的结点
        l.next = newNode;
    size++;
    modCount++;
}
public E remove() {
    return removeFirst();
}
public E removeFirst() {
    final Node<E> f = first;
    //如果删除的是空结点,则会报异常
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
//核心逻辑,将双向链表的第一个结点删除
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;  //取出内容
    final Node<E> next = f.next; //取出要删去的结点的下一个结点
    f.item = null;
    f.next = null; // help GC  GC垃圾处理机制处理
    first = next;  //把first指向后移
    if (next == null)  //如果删除后链表为空,则让last也为空
        last = null;
    else   //first指向的不为空,last不用修改,只需要则将next的prev指向设置为空
        next.prev = null;
    size--; //元素个数--
    modCount++; //修改次数++
    return element;
}
数据结构 底层结构 启用版本 线程安全 扩容倍数
ArrayList 可变数组 jdk1.2 不安全,增删效率低,改查效率高 有参,按照1.5倍扩容;无参,第一次扩容10,之后按照1.5倍扩容
Vector 可变数组 jdk1.0 安全,效率不如ArrayList 无参,默认10,然后按照2倍扩容,有参,按照2倍扩容
LinkedList 双向链表 不安全,增删效率高,改查效率低

Set?

Set接口对象 -- 即实现了set接口的类的对象

  • Set 不可以放重复的元素,且存放数据是无序的,取出的数据的顺序不是添加时的顺序,但也只会是这个顺序

    HashSet set = new HashSet();
    set.add("vicen");
    set.add("jack");
    set.add(null);
    System.out.println(set);  // [null, vicen, jack]
    
  • 遍历方式有两种,增强for循环和迭代器遍历,不能用索引(因为set是无需的)

HashSet

  • 执行add方法时,添加成功则返回true,失败返回false (null也不可以重复)

  • HashSet的底层就是HashMap 数组+链表+红黑树

    public static void main(String[] args) {
        //模拟HashSet的底层
        //创建一个Node数组
        Node[] table = new Node[16];
        System.out.println("table=" + table);
        //创建结点
        Node node = new Node("john",null);
        table[2] = node;
        Node node1 = new Node("svicen",null);
        node.next = node1;
    }
    class Node{
        Object item;
        Node next;
        public Node(Object item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
    
  • HashSet的add方法的底层源码剖析

    • HashSet的底层就是HashMap,添加一个元素时,先得到hash值,再将hash值转为索引值
    • 找到存储数据表table,看这个索引位置是否已经存放元素 如果hash值相同就比较内容是否相同,内容不同就加到最后
    • 如果没有则直接加入
    • 如果有,调用equals方法比较,如果相同就放弃添加,equals比较的内容可以自己定义,如果不相同就添加到最后一个Node的next
    • 在Java8中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(8) 并且table表的大小大于MIN_TREEIFY_CAPACITY(64) 就会进行优化(优化为红黑树)
    //自己写的代码
    public static void main(String[] args) {
        HashSet set = new HashSet();    //断点
        set.add("java");
        set.add("php");
        set.add("java");
    }
    public HashSet() {
        map = new HashMap<>();
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null; //第一次插入返回true  因为map有key-value  而set只用了key
        //PRESENT为HashSet的一个私有属性,起到占位作用,private static final Object PRESENT = new Object();
    }
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); //第一次add,返回null
    }
    static final int hash(Object key) {  
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  //得到key对应的hash值,并不是hashcode
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;  // 这里resize返回后  tab的长度就是16了
        //计算根据key值得到的hash去计算一个存放到table表的哪一个位置,并把这个位置的对象赋给p
        //判断p是否为空
        //如果p为null,表示还没有存放元素,直接创建一个Node放在tab[i]
        if ((p = tab[i = (n - 1) & hash]) == null) //i = (n - 1) & hash 求索引  n就是数组容量 16
            //(n - 1) & hash  =  hash % n 位与运算可以实现和取模运算相同  而位运算效率更高
            tab[i] = newNode(hash, key, value, null);  //key就是传进来的"java"   value永远都是属性  PRESENT
        //当添加的位置已经有元素时 走这里
        else {
            Node<K,V> e; K k;
            //关键代码
            //要add的元素与 当前索引位置对应的链表的第一个对象p的hash相同 
    //并且满足   加入的key和p指向的node的key是同一对象 或者 不是一个对象但是内容相同(由重写的equals决定) key.equals(k)
            //就不能加入
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;  // 不能加入 把当前节点赋给e
            //判断 p 是否是一棵红黑树   是的话调用putTreeVal进行添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
            //p指向的是一个链表  但还没有优化为红黑树
            else {
                //依次对链表的元素进行比较  如果都不相同,就添加到最后
                for (int binCount = 0; ; ++binCount) {
                    //走到链表末尾
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null); //插入到最后 
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  链表数量大于默认值后优化为红黑树
                            treeifyBin(tab, hash); // TREEIFY_THRESHOLD - 1 即 8 - 1
                        //在treeifyBin函数内部还有判断   当tab.length >= MIN_TREEIFY_CAPACITY(64)时再优化 <则 resize
                        break;   //添加成功 退出循环
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;  //有一个相同直接 break; 无法加入
                    p = e;   //指针后移 
                }
            }
            //这里表示add不成功,返回oldValue
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;  //修改次数++
        if (++size > threshold)  //threshold为12,加载因子,防止到达容量上限再扩容
            resize();
        afterNodeInsertion(evict); //给HashMap的子类提供的方法
        return null;        //add成功,返回null  
    }
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;  //默认的table表的大小16  1 << 4 之所以取16是为了降低hash碰撞的几率
            // 0.75 * 16 加载因子0.75 用到12后开始准备扩容
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
    • HashSet底层是HashMap,第一次添加时,table数组扩容为16,临界值(threshold) 为16 * 加载因子(loadFactor 0.75) = 12
    • 如果table数组大于临界值12就会扩容到16*2=32,数组大小包括(数组元素和链表内元素)新的临界值就是32 *0.75=24 依次类推
    • 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(8) 并且table表的大小大于MIN_TREEIFY_CAPACITY(64) 就会进行优化(优化为红黑树)
    • 如果已经一条链表的元素个数超过8,但table表大小还没有超过64,再加入元素时会对table进行resize,扩容为2倍,直到table表大小超过64后优化为红黑树,将Node优化为TreeNode
作业例题?
public class Homework04 {
    public static void main(String[] args) {
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");
        HashSet hashSet = new HashSet();
        hashSet.add(p1);
        hashSet.add(p2);
        System.out.println("删除前: " + hashSet);
        p1.setName("CC"); //这里修改了p1的属性
        //因为上面修改了p1的name,所以删除时根据Person的id和name查找的索引不再是原来的p1的位置,无法将p1删除
        hashSet.remove(p1);
        System.out.println("删除后: " + hashSet); // 2个对象,没有删除
        hashSet.add(new Person(1001,"CC"));
        System.out.println(hashSet);  // 3个对象
        hashSet.add(new Person(1001,"AA"));
        System.out.println(hashSet); //4个对象,因为上面p1的name已经修改,加入时比较时equals方法判断不相等,可以加入
    }
}
/*
删除前: [Person{id=1002, name='BB'}, Person{id=1001, name='AA'}]
删除后: [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
*/
class Person{
    private int id;
    private String name;
    //注意这里需要重写Person 的equals和hashCode方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id && Objects.equals(name, person.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}
LinkedHashSet
  • 实现了HashSet的子类,底层是LinkedHashMap,底层维护了一个数组+双写链表

  • LinkedHashSet根据元素的hashcode值来决定元素的存储位置,同时用链表维护元素的次序,使得元素看起来是以插入顺序排序的

  • 每插入一个元素会将该元素的pre指向前一个结点,前一个结点的next指向插入的结点

  • 效率不入HashSet,但取数据时是有序的

    LinkedHashSet linkedHashSet = new LinkedHashSet();
    linkedHashSet.add(123);
    linkedHashSet.add("abc");
    linkedHashSet.add(456);
    linkedHashSet.add(123);
    System.out.println("linkedhashset =" + linkedHashSet);  // linkedhashset =[123, abc, 456]
    
  • 第一次添加时,直接将table扩容到16,存放的结点类型是LinkedHashMap$Entry,数组类型是HashMap$Node[]

     //LinkedHashMap的静态内部类Entry    继承了HashMap的一个静态内部类Node
    static class Entry<K,V> extends HashMap.Node<K,V> { 
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    

TreeSet

  • TreeSet底层还是TreeMap

  • TreeSet可以排序,利用自己定义的匿名内部类compartor,在add元素时,在TreeSet底层会调用自己定义的compartor方法

    TreeSet treeSet = new TreeSet(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            //下面调用String 的compareTo方法进行字符串大小比较
            return ((String) o1).compareTo((String) o2);
        }
    });
    treeSet.add("tom");
    treeSet.add("jack");
    treeSet.add("abc");
    System.out.println(treeSet);
    //构造函数内有匿名内部类时 
    //TreeSet接收了实现了comparator接口的对象  然后将其传给了TreeMap
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;  // 实现了comparator接口对象 赋给了TreeMap的一个属性comparator
    }
    // put函数内的关键代码   cpr其实就是我们实现的匿名内部类的compare方法
    //这里的put方法其实就是HashMap的put方法,此时的value值为TreeSet的静态对象属性  PRESENT
    public V put(K key, V value) {
        Entry<K,V> t = root;
        //第一次加入时 t为null
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);  //根据compare方法决定添加元素的顺序
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);  //当两个元素cmp后为0时,即相等直接返回0,key无法加入
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    

TreeSet 与 HashSet比较

二者去重比较:

  • HashSet:hashCode()+equals(),当add元素时,通过运算得到一个hash值,然后根据hash值得到一个索引,如果该索引位置为空,则直接加入,如果有数据,则进行equals比较(遍历比较),如果比较后结果为0表示相同,则不加入(equals方法具体比较什么由程序员自己决定)

  • TreeSet:如果传入了一个Comparator匿名对象,就使用该匿名对象实现的compare方法去重,如果结果返回0,就认为是相同的元素,就不添加。如果没有传入一个Comparator匿名对象,就以添加的对象实现的Comparable接口的compareTo方法去重

    如果添加的对象没有实现Comparable接口,Comparable<? super K> k = (Comparable<? super K>) key 向上转型时会报错

HashSet LinkedHashSet TreeSet
按hash值定索引,取出与插入顺序往往不同 有pre和next属性,使得元素看起来是以插入顺序排序的 可以自己指定排序的方法
底层其实就是HashMap 继承了HashSet 底层其实就是TreeMap

Map

  • Map与Collection并列存在,用于保存具有映射关系的双列数据类型:key-value
  • Map的key和value可以是任何引用类型的数据,会封装到 HashMap$Node对象中
  • Map的key值不允许重复,放入已有key值的对象时会覆盖原来的,但是Map的value值可以重复
  • Map的key值可以为null,但也只能有一个为null,value可以有多个null

HashMap

  • HashMap没有实现同步,因此是线程不安全的

    HashMap hashMap = new HashMap();
    hashMap.put("no1","张无忌");
    hashMap.put("no2","张三丰");
    hashMap.put(null,"123");
    hashMap.put(1,"数字");
    hashMap.put(new Object(),1);  //key可以为任何Object类型
    //get方法取出key为指定值的元素  返回值是对象
    System.out.println(hashMap.get(1));
    
  • 一对key-value是放在一个HashMap$Node中的,因为Node实现了Entry接口,有些也说一对key-value就是一个Entry

    static class Node<K,V> implements Map.Entry<K,V>{
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    
  • k-v为了方便程序员的遍历,还会创建一个entrySet集合,存放的类型为Entry,Set<Map.Entry<K,V>> entrySet

    注意这里的K和V并没有重新创建,只是指向了HashMap$Node的K和V,定义的虽然是entry,但实际上类型还是Node

    Set set = hashMap.entrySet();
    System.out.println(set.getClass());   //HashMap$EntrySet
    for (Object obj : set) {
        System.out.println(obj.getClass()); //HashMap$Node
        Map.Entry entry = (Map.Entry) obj;  //向下转型,调用子类方法
        System.out.println(entry.getKey()+ "-" + entry.getValue());  // 方便遍历
    }
    //为了方便遍历,Entry有以下方法
    interface Entry<K, V> {
        K getKey();
        V getValue();
        V setValue(V value);
    }
    

    Entry中存放的只是指向hashMap的table表的元素,再把Entry放在EntrySet集合里,

    //自动补全后如下,可知keySet的编译类型为Set,而hashMap.values()的类型为Collection
    //Entry内的key放在keySet集合里,而value放在Collection里  也是指向HashMap$Node的元素
    Set set1 = hashMap.keySet();  //可以取出Enrty的所有的key,赋给Set集合对象
    Collection values = hashMap.values();
    

    image-20220501133231573

  • 方法

    //判断键是否存在  containsKey
    System.out.println(hashMap.containsKey("no1"));
    //移除键值对关系,以key为索引
    hashMap.remove(1);
    //键值对个数  size
    System.out.println(hashMap.size());
    //三种遍历方法
    //第一种,通过keySet
    Set keyset = hashMap.keySet();
    //增强for
    for (Object key :keyset) {
        System.out.println(key + "--" + hashMap.get(key));
    }
    //迭代器  只要继承了Collection接口的都有iterator迭代器
    Iterator iterator = keyset.iterator();
    while (iterator.hasNext()) {
        Object key =  iterator.next();
        System.out.println(key + "--" + hashMap.get(key));
    }
    //第二种,通过values
    System.out.println("-----第二种------");
    Collection values = hashMap.values();
    //增强for
    for (Object value : values) {
        System.out.println(value);
    }
    //迭代器  只要继承了Collection接口的都有iterator迭代器
    Iterator iterator1 = values.iterator();
    while (iterator1.hasNext()) {
        Object value = iterator1.next();
        System.out.println(value);
    }
    //第三种,通过entrySet
    System.out.println("-----第三种-----");
    Set set1 = hashMap.entrySet();
    //增强for
    for (Object entry : set1) {
        Map.Entry me =  (Map.Entry) entry;
        System.out.println(me.getKey() + "--" + me.getValue());
    }
    //迭代器  只要继承了Collection接口的都有iterator迭代器
    Iterator iterator2 = set1.iterator();
    while (iterator2.hasNext()) {
        Object m = iterator2.next();
        Map.Entry me =  (Map.Entry) m;
        System.out.println(me.getKey() + "--" + me.getValue());
    }
    

Hashtable

  • Hashtable的key和value都不可以为空

  • Hashtable是线程安全的,put方法有synchronize关键字,但效率没有HashMap高

  • 底层有一个数组Hashtable$Entry[] , Hashtable的内部类Entry,实现了Map.Entry接口,初始容量为11 临界值11*0.75=8

  • 扩容:当数组大小等于临界值11*0.75 = 8时会扩容,if (count >= threshold) newCapacity = (Oldcapacity << 1) + 1

    扩容的大小为 两倍+1

?以上所有扩容机制都是先检查是否达到threshold然后再将要加入的元素加进table表,然后count++

由于每次判断的都是count >= threshold 所以在加入第八个元素时count还为7,不会扩容

Properties

  • 继承了HashTable类,并且实现了Map接口,也是使用键值对形式来保存数据
  • Properties还可以从xxx.properties文件中,加载数据到Properties对象并进行读取和修改
  • 一般情况下,xxx.properties文件往往为配置文件。

TreeMap

  • TreeMap内有属性compartor,可以按照自己指定的规则而进行排序

    TreeMap treeMap = new TreeMap(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            //按照传入的key的大小进行排序
            return ((String) o2).compareTo((String) o1);
        }
    });
    treeMap.put("jack","杰克");
    treeMap.put("smith","史密斯");
    treeMap.put("tom","汤姆");
    System.out.println(treeMap);
    }
    //构造器处下断点,源码如下
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    public V put(K key, V value) {
        Entry<K,V> t = root;
        //第一次添加元素时下面这个判断为true  最终返回null   第二次添加时root即为第一次添加的元素
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {  //遍历所有的key,找当前的key应该添加的位置
                parent = t;
                //动态绑定到我们的匿名内部类的compare
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    //如果找到相等的,则把当前Entry的value设置为新添加的元素的value值,但key值不会改变
                    return t.setValue(value);
            } while (t != null);
        }
    

Collection工具类

  • 是一个操作集合类型的工具类,内置了很多对List,Set,Map的操作的静态方法,reverse,shuffle(随机排序),sort,swap,max
ArrayList arrayList = new ArrayList();
arrayList.add("tom");
arrayList.add("smith");
arrayList.add("king");
arrayList.add("milan");
System.out.println(arrayList);
Collections.reverse(arrayList);
System.out.println(arrayList);
Collections.sort(arrayList);
System.out.println(arrayList);
//自己指定排序方式
Collections.sort(arrayList, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        //校验代码
        if (o1 instanceof String && o2 instanceof String) {
            return ((String) o1).length() - ((String) o2).length();
        }
        return 0;
    }
});
System.out.println(arrayList);
// 交换指定位置元素
Collections.swap(arrayList,1,2);
//求指定排序方式的最大元素
System.out.println(Collections.max(arrayList));
System.out.println(Collections.max(arrayList, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o1).length() - ((String)o2).length();
    }
}));
//查找出现次数
System.out.println("tom的次数" + Collections.frequency(arrayList,"tom"));
//拷贝
ArrayList dest = new ArrayList();
for (int i = 0; i < arrayList.size(); i++) {
    dest.add(i);
}
Collections.copy(dest,arrayList);  //这里需要先把dest的数组大小设置为与原来的arrayList大小相同,不然会抛出数组越界异常
System.out.println(dest);
//替换
Collections.replaceAll(arrayList,"tom","汤姆");

泛型

  • 泛型又称参数化类型,是jdk5后出现的新特性,用于解决数据类型的安全性问题
  • 在类声明或实例化时只要指定好需要的具体的类型即可,如果没有指定具体的数据类型默认是Object
  • 泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回值的类型,或者是参数类型
  • 泛型可以保证如果在编译时没有错误,那么在运行时就不会出现类型转换异常(ClassCastException)
//泛型入门,指定泛型的具体数据类型时 只能用引用类型,不能用基本类型
Person<String, Integer> hs = new Person<>("hs", 123);   //建议这样写
//Person<String, Integer> hs = new Person<String, Integer>("hs", 123);  后边的<>里类型可以省略,且推荐省略
System.out.println(hs);
HashMap<String, Person> stringPersonHashMap = new HashMap<>();
//这样也可以 HashMap<String, Person<String,Integer>> stringPersonHashMap = new HashMap<>();
stringPersonHashMap.put("jack",new Person("jak",123));
System.out.println(stringPersonHashMap);
Person person = new Person("123", 123);  //创建Person实例化对象时也可以不显示给出变量类型声明,直接用特定变量填充E、T即可
class Person<E,T>{
    private E name;
    private T age;
}
@SuppressWarnings({"all"})
public class GenericExercise01 {
    public static void main(String[] args) {
        ArrayList<Employee> employees = new ArrayList<>();
        employees.add(new Employee("jack",30000,new MyDate(2000,6,19)));
        employees.add(new Employee("tom",10000,new MyDate(2001,6,15)));
        employees.add(new Employee("tom",20000,new MyDate(2001,5,17)));
        System.out.println(employees);
        //下面是对birthday的比较,最好将其放在MyDate类完成
        employees.sort(new Comparator<Employee>() {
            @Override
            public int compare(Employee emp1, Employee emp2) {
                //先对传入的参数进行验证
                if (!(emp1 instanceof Employee && emp2 instanceof Employee)) {
                    System.out.println("类型不正确");
                    return 0;
                }
                //先按名字排序,如果名字相同就比较出生日期
                int i = emp1.getName().compareTo(emp2.getName());
                if (i != 0){
                    return i;
                }
                //为了提高程序的可读性和封装性,比较的具体操作再MyDate类中实现
                return emp1.getBirthday().compareTo(emp2.getBirthday());
            }
        });
        System.out.println("=====排序后======");
        System.out.println(employees);
    }
}
//继承Comparable接口,传入参数类型为MyDate
class MyDate implements Comparable<MyDate>{
    private int year;
    private int month;
    private int day;
    @Override
    public int compareTo(MyDate o) {
        //如果 name 相同,比较birthday,依次比较年、月、日
        int yearMinus = year - o.getYear();
        if (yearMinus != 0){
            return yearMinus;
        }
        int monthMinus = month - o.getMonth();
        if (monthMinus != 0){
            return monthMinus;
        }
        return day - o.getDay();
    }
}
class Employee{
    private String name;
    private double sal;
    private MyDate birthday;
}

自定义泛型

  • 普通成员可以使用泛型(属性,方法)
  • 使用泛型的数组不能初始化,在泛型类内不可以初始化
  • 静态方法中不能使用类的泛型,有可能加载类时对象仍未创建,但具体的类型在创建对象时才会指定
  • 泛型类的类型,是在创建对象的时候确定的(创建对象时,需指定类型,如果没有默认是Object)
  • 泛型标识符:T、R、M、E ...

自定义接口

  • 在接口中,静态成员也不能使用泛型,与自定义泛型一样
  • 泛型接口的类型,在继承接口或实现接口的时候确定
  • 没有指定类型,默认是Object
interface IA extends IUsb<String,Double>{
}
//这里继承了IA,也就相当于指定了IUsb的泛型的类型,在实现IUsb方法时,使用String替换U,Double替换R
//如果直接继承了IUsb,则需要自己在继承的时候或者实现接口方法的时候指定类型
class AA implements IA{
    @Override
    public Double get(String s) {
        return null;
    }
    @Override
    public void hi(Double aDouble) {
    }
    @Override
    public void run(Double r1, Double r2, String u1, String u2) {
    }
}
interface IUsb<U,R>{
    //U u;  错误,接口的变量隐式的指定为 public static final   静态成员不能使用泛型
    int n = 10;  //这是正确的
    //普通方法中,可以使用接口泛型
    R get(U u);
    void hi(R r);
    void run(R r1,R r2,U u1, U u2);
    //可以在接口中使用默认方法,默认方法也是可以使用泛型的
    default R method(U u) {
        return null;
    }
}

泛型方法

  • 泛型方法可以定义在泛型类里,也可以定义在普通类里

    public class CustomMethodGeneric {
        public static void main(String[] args) {
            Car car = new Car();
            car.fly("宝马",123);
            Fish<String, ArrayList> stringArrayListFish = new Fish<>();
            stringArrayListFish.hello("jack",1.23f); //使用泛型方法时第一个参数的具体类型已经确定了,自己指定第二个类型
        }
    }
    class Car{
        //普通方法
        public void run(){
        }
        //泛型方法
        public <T,R> void fly(T t,R r) {
        }
    }
    class Fish<T,R>{
        //普通方法
        public void run(){
        }
        //泛型方法,泛型标识符最好与类的泛型标识符区分开
        public <M,U> void eat(M m, U u){ 
        }
        //这个方法并不是泛型方法,定义的前面并没有<>指定泛型标识符,而是方法使用了泛型
        public void hi(T t){
        }
         //泛型方法可以使用类声明的泛型,也可使用自己声明的泛型
        public <K> void hello(T t, K K){
        }
    }
    

泛型继承与通配符

  • 泛型没有继承性

    List <Object> list = new ArrayList<String> ();  //错误,泛型不具有继承性
    
  • 通配符

    • :支持任意泛型类型
    • :支持A类以及A类的子类,规定了泛型的上限A
    • :支持A类以及A类的父类,不限于直接父类,规定了泛型的下限A
//可以接收任意泛型类型
public static void printCollection(List<?> c){
    for (Object o :c) {
        System.out.println(o);
    }
}

JUnit使用

  • 一个类中有很多功能代码需要测试,为了测试就需要写到main方法中
  • 如果有多个功能代码测试,就需要来回注销,切换很麻烦
  • 如果可以直接运行一个方法,就方便很多,并且可以给出相关信息。
  • java提供了一个类JUnit,是一个Java语言的单元测试框架
public class Junit_ {
    public static void main(String[] args) {
    }
    @Test  //输入@Test后 alt + enter 添加 JUnit5.8  之后会引入包  之后就可以直接运行该方法了,也可以使用快捷键
    public void m1(){ 
        System.out.println("m1被调用");
    }
    @Test
    public void m2(){
        System.out.println("m2被调用");
    }
}
  • 作业题
@SuppressWarnings({"all"})
public class Homework01 {
    public static void main(String[] args) {
    }
    @Test
    public void testList(){
        //这里给T的指定类型为User
        DAO<User> dao = new DAO<>();
        dao.save("001",new User(1,18,"jack"));
        dao.save("002",new User(2,28,"rom"));
        dao.save("003",new User(3,38,"smith"));
        //拿到返回的所有对象
        List<User> list = dao.list();
        System.out.println("list=" + list);
        //修改对象
        dao.update("003",new User(15,55,"milan"));
        //获取修改后的所有对象
        list = dao.list();
        System.out.println(list);
        System.out.println(dao.get("003"));
    }
}
@SuppressWarnings({"all"})
class DAO<T>{
    private Map<String,T> map = new HashMap<>();
    //存放元素
    public void save(String id, T entity) {
        map.put(id,entity);
    }
    public T get(String id){
        return map.get(id);
    }
    //更改对象value
    public void update(String id,T entity) {
        map.put(id,entity);
    }
    //返回map里的对象
    public List<T> list(){
        //遍历map的key,找到所有的value,然后封装到ArrayList返回即可
        List<T> list = new ArrayList<>();
        //遍历map
        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            list.add(get(key));
            //list.add(map.get(key)); 这样也可以
        }
        return list;
    }
}
class User{
    private int id;
    private int age;
    private String name;
}

绘图

package com.svicen.draw_;
import javax.swing.*;
import java.awt.*;
//要想绘图,需要继承JFrame  在窗口内嵌入面板
public class DrawCircle extends JFrame{
    //定义一个面板
    private MyPanel mp = null;
    public static void main(String[] args) {
        new DrawCircle();
    }
    public DrawCircle(){
        //初始化面板
        mp = new MyPanel();
        //面板放入到窗口(就是画框)
        this.add(mp);
        this.setSize(400,300);  //400*300 像素
        //当关闭弹出的组件后 jvm并不会释放这个JFrame对象  在这里设置当点击组件的关闭x后程序完全退出
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);  //设置可以显示
    }
}
//先定义一个MyPanel,继承java的JPanel类  在画板上画图形
class MyPanel extends JPanel {
    //Graphics 可以认为是一个画笔  Panel是一个面板
    @Override
    /*   paint方法调用有四种情况
    1.当组件第一次在屏幕显示时,系统自动调用  
    2.窗口最小化后再最大化
    3.窗口的大小发生变化
    4.repaint函数被调用
    */
    public void paint(Graphics g) {
        super.paint(g); //调用父类的方法完成初始化
        //画一个圆形 四个参数 x,y,width,height
        g.drawOval(10,10,100,100); 
    }
}
  • 绘图方法
//画一个圆形 四个参数 x,y,width,height  x y为圆的边界的坐标
g.drawOval(10,10,100,100);
//画直线  四个参数 x1 x2 y1 y2
g.drawLine(10,10,100,100);
//绘制矩形边框  四个参数 x y width height
g.drawRect(10,10,100,100);
//绘制填充矩形,先指定颜色
g.setColor(Color.blue);
g.fillRect(10,10,100,100);
//绘制填充圆形,可以使椭圆
g.fillOval(10,10,100,100);
//获取图片资源  这里getResource在jdk9之后做了修改
Image image = Toolkit.getDefaultToolkit().getImage(MyPanel.class.getResource("/1.png"));
g.drawImage(image,10,10,28,28,this);
//绘制字符串,即写字
g.setColor(Color.red);
g.setFont(new Font("隶书",Font.BOLD,50)); //字体,是否加粗,字体大小
g.drawString("你好",100,100); //字体的左下角坐标
  • Java事件处理

    java事件处理是采取"委派事件模型"。当事件发生时,会把此信息传递给事件的监听者处理,这里所说的信息实际就是java.awt.event事件类库的某个类所创建的对象,把它称为事件的对象

public class BallMove extends JFrame {
    MyPanel mp = null;
    public static void main(String[] args) {
        new BallMove();
    }
    public BallMove(){
        mp = new MyPanel();
        this.add(mp);
        this.setSize(400,300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //使窗口的的JFrame对象可以监听到面板上发生的键盘时间   这里必须要add进去
        this.addKeyListener(mp);
        this.setVisible(true);
    }
}
//implements KeyListener  实现监听键盘的接口
class MyPanel extends JPanel implements KeyListener {
    //为了让小球可以移动,我们需要把小球左上角的坐标设置为变量
    int x = 10;
    int y = 10;
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillOval(x,y,20,20);
    }
    //当某个键按下时该方法触发
    @Override
    public void keyPressed(KeyEvent e) {
        //System.out.println((char) e.getKeyCode() + "被按下");
        if(e.getKeyCode() == KeyEvent.VK_DOWN) {
            //向下的键被按下
            y++;
        } else if (e.getKeyCode() == KeyEvent.VK_UP) {
            y--;
        } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            x--;
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            x++;
        }
        //改变后需要让面板重绘
        this.repaint();
    }
}

详情见坦克大战代码

线程

image-20220505100344887

线程使用

继承Thread类
  • 线程类需要重写run方法,Thread类并没有run方法,Thread类实现了Runnable的run接口

    //Thread类的源码
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    //自己写的代码
    public class Thread01 {
        public static void main(String[] args) throws InterruptedException {
            Cat cat = new Cat();
            cat.start();    //调用cat线程类的start方法后,它会自动调用自己的run方法
    //如果直接调用cat的run方法,则不会有多线程,相当于用main线程把run方法执行完后再接着执行main函数的内容,会阻塞
            for (int i = 0; i < 10; i++) {
                System.out.println("main " + i);
                //让主线程休眠
                Thread.sleep(1000);
            }
        }
    }
    //当一个类继承了Thread类,该类就可以当成线程类使用
    class Cat extends Thread{
        int times = 0;
        @Override
        public void run() {  //重写run方法,写自己的业务逻辑
            while(true) {
                System.out.println("喵喵" + (++times));
                try {
                    //线程休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(times == 5) {
                    break;
                }
            }
        }
    }
    
    • 进程启动后首先会开启一个main线程,main线程又 start 一个子线程Thread-0,主线程并不会阻塞,多CPU并行,单CPU并行

    • 可以使用JConsole把线程执行情况图形化展示出来,终端输入jconsole

      image-20220505124804687

    • 主线程main结束并不代表程序已经结束了,如果子线程比主线程执行的时间长也照样可以执行

    • 同样,子线程也可以再开子线程,只有所有的子线程全部执行完成后,进程才会结束

    //start底层源码
    public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
        group.add(this);
        boolean started = false;
        try {
            start0();  //start0由JVM机调用,由c/c++编写, 关键,线程同步都是靠这个方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
            }
        }
    
    • 底层的start0()方法才是真正实现多线程的函数,所以要通过调用start方法而不是直接调用run方法
实现接口Runnable
  • 主要用于对继承了其他类的类实现线程,因为Java只能单继承
  • 实现接口的线程类没有start方法,只能通过创建线程对象来调用start
public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //dog.start();   由于实现接口的线程类没有start方法
        Thread thread = new Thread(dog);
        thread.start();
    }
}
class Dog implements Runnable{
    int count = 0;
    @Override
    public void run() {
        while(true){
            System.out.println("小狗汪汪叫" + (++count) + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(count == 10){
                break;
            }
        }
    }
}
  • 为什么调用Thread类的对象的start方法可以调用dog类的run方法呢,底层使用了一种设计模式:代理模式(静态代理)

  • 代码模拟实现Thread类的多线程实现过程

    public class Thread02 {
        public static void main(String[] args) {
            Tiger tiger = new Tiger();
            Thread thread = new Thread(tiger);
            thread.start();
        }
    }
    class Animal{}
    class Tiger extends Animal implements Runnable{
        int count = 0;
        @Override
        public void run() {
            while (true){
                System.out.println("老虎嗷嗷叫" + (++count) + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(count == 10){
                    break;
                }
            }
        }
    }
    
  • 实现Runnable接口的方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。建议使用

通知线程退出

public class ThreadExit {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();
        //如果希望main线程去控制t1线程的终止,可以修改loop为false 让t1退出run方法,从而终止t1线程
        //主线程休眠5秒,退出t1线程
        Thread.sleep(5000);
        t1.setLoop(false);
    }
}
class T extends Thread{
    private int count = 0;
    private boolean loop = true;
    @Override
    public void run() {
        while(loop){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("T 运行中" + (++count));
        }
    }
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

线程方法

setName 设置线程名称,使之与参数name相同

getName 返回该线程的名称

start 使该线程开始执行;Java虚拟机底层调用该线程的start0方法

run 调用线程对象run方法;

setPriority 更改线程的优先级

getPriority 获取线程的优先级

sleep 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

interrupt 中断线程,而不是终止线程,如果正在休眠则提前终止休眠

public static void static yield() 暂停当前正在执行的线程对象,并执行其他线程。礼让进程,但礼让的时间不确定,不一定礼让成功

public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。

public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。插入的线程一旦插队成功,则肯定先执行完插入的线程所有的任务

public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

public static Thread currentThread() 返回对当前正在执行的线程对象的引用。

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //测试相关的方法
        T t = new T();
        t.setName("jack");
        t.setPriority(Thread.MIN_PRIORITY);//设置优先级为 1 最小优先级
        t.start();//启动子线程
        //主线程打印5次hi ,然后就中断子线程的休眠
        for(int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }
        System.out.println(t.getName() + " 线程的优先级 =" + t.getPriority());//1
        t.interrupt();//当执行到这里,就会中断 t线程的休眠.  提前结束休眠
    }
}
class T extends Thread { //自定义的线程类
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                //Thread.currentThread().getName() 获取当前线程的名称
                System.out.println(Thread.currentThread().getName() + " 吃东西~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);    //20秒
            } catch (InterruptedException e) {
                //当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以在这里加入自己的业务代码
                //InterruptedException 是捕获到一个中断异常.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}
  • 线程插队与礼让
public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        T3 t3 = new T3();
        t3.start();
        for(int i = 1; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主线程(小弟) 吃了 " + i  + " 包子");
            if(i == 5) {
                System.out.println("主线程(小弟) 让 子线程(老大) 先吃");
                //join, 线程插队
                t3.join();// 这里相当于让t2 线程先执行完毕  调用的是对方的join方法,join到自己的线程里
               //Thread.yield();//礼让,不一定成功 如果内核资源不紧张,可以满足两个线程  那就不会礼让
                System.out.println("子线程(老大) 吃完了 主线程(小弟) 接着吃..");
            }
        }
    }
}
class T3 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程(老大) 吃了 " + i +  " 包子");
        }
    }
}

用户线程:也叫工作线程,当线程的任务执行完毕或以通知方式结束(即上面的退出方式)

守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,一般用于监视某线程

常见的守护线程:垃圾回收机制。只要还有线程在工作,垃圾回收机制就一直会守护。

线程七大状态

JDK 中用 Thread.State 枚举表示了线程的六种状态

Java学习笔记

线程状态转换图(RUNNABLE又可以细分为两种状态(Ready和Running))

Thread_state

使用程序查看线程状态
创建 T 线程,然后输出此时的状态,再启动线程,利用循环,查看线程状态,只要线程没终止,就会不停的输出状态

public class Thread_State {
    public static void main(String[] args) throws InterruptedException {
        T4 t4 = new T4();
        System.out.println(t4.getName() + " 状态 " + t4.getState()) ;
        t4.start();
        while(t4.getState() != Thread.State.TERMINATED) {
            System.out.println(t4.getName() + " 状态 " + t4.getState());
            Thread.sleep(1000);
        }
        System.out.println(t4.getName() + " 状态 " + t4.getState());
    }
}
class T4 extends Thread{
    @Override
    public void run() {
        while(true){
            for (int i = 0; i < 10; i++) {
                System.out.println("hi  " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            break;
        }
    }
}

线程同步

同步具体方法:Synchronized

public class SellTickets {
    public static void main(String[] args) {
        SellTicket02 sellTicket02 = new SellTicket02();
        //这里会出现票数超卖现象,需要进行线程互斥的限制
        //同一个对象开启三个线程
        new Thread(sellTicket02).start();
        new Thread(sellTicket02).start();
        new Thread(sellTicket02).start();
    }
}
//使用同步方法Synchronized实现线程同步
class SellTicket02 implements Runnable{
    private static int ticketNum = 100;
    private boolean loop = true;
    //这里就是一个同步方法,锁的对象为  this对象 ,也可以用同步代码块实现
    public synchronized void sell(){
        if(ticketNum <= 0) {
            System.out.println("票已售空");
            loop = false;
            return;
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" +
                "  剩余票数" + (--ticketNum));
    }
    public /*synchronized*/ void sell(){
        synchronized (this) {
            if (ticketNum <= 0) {
                System.out.println("票已售空");
                loop = false;
                return;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("窗口" + Thread.currentThread().getName() + "售出一张票" +
                    "  剩余票数" + (--ticketNum));
        }
    }
    //静态方法实现同步代码块,不能用于锁this,只能锁  类名.class
    public static void m(){
        synchronized (/*this*/SellTicket02.class){
            System.out.println("静态方法锁");
        }
    }
    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}
互斥锁
  • 一般选择同步代码块(上锁的代码量尽量小)
  • 要求多个线程的锁对象必须为同一个,如果用extends Thread实现的线程类,需要创建多个对象,不满足互斥锁
  • Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
  • 每个对象都对应一个可称为互斥锁的标记,这个标记用来保证在任意时刻,只能有一个线程访问该对象
  • 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
  • 同步的局限性:导致程序的执行效率降低
  • 同步方法(非静态)的锁可以是this,也可以是其他对象(要求是同一个对象):比如说Object等父类
  • 同步方法(静态)的锁为当前类本身。(因为静态方法可以通过类直接调用)
  • this对象锁是非公平锁

线程死锁

多个线程都占用了对方的锁资源,但不肯相认,导致了死锁

释放锁

以下情况会释放锁

  • 同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到break,return
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception导致异常结束
  • 当前线程同步代码块、同步方法中执行了线程的wait()方法,当前线程暂停,并释放锁

以下情况不会释放锁

  • 当前线程在同步代码块、同步方法中调用sleep方法和yield方法不会释放锁
  • 当前线程执行同步代码块、同步方法时,其他线程调用了该线程的suspend方法将该线程挂起(由Running到Ready状态),该线程不会释放锁, suspend和resume两个方法已经不再使用

IO流

文件的创建

  • createNewFile()方法来创建一个新文件
//方式一  直接写明文件路径 public File(String pathname)
@Test
public void create01(){
    String filePath = "D:\\JetBrains\\hello.txt";
    File file = new File(filePath); //只是创建了一个file对象但是还没有创建具体的文件
    try {
        file.createNewFile();
        System.out.println("hello1文件创建成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
@Test
//方式二  通过父目录文件 + 子路径 public File(File parent, String child)
public void create02(){
    File parentfile = new File("D:\\JetBrains\\");
    String fileName = "hello2.txt";
    File file = new File(parentfile, fileName);
    try {
        file.createNewFile();
        System.out.println("hello2创建成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
@Test
//方式三 通过父目录 + 子文件路径   public File(String parent, String child) 用于在一个父目录下创建多个子文件
public void create03(){
    String parentPath = "D:\\JetBrains\\";
    String fileName = "hello3.txt";
    File file = new File(parentPath, fileName);
    try {
        file.createNewFile();
        System.out.println("hello3创建成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

常用的文件操作

方法名称 说明
public String getName() 返回由此抽象路径名表示的文件或文件夹的名称
public booleean isFile() 测试此抽象路径名表示的File是否为文件
public boolean isDirectory() 测试此抽象路径名表示的File是否为文件夹
public String getAbsolutePath() 返回此抽象路径名的绝对路径名字符串
public String getPath() 将此抽象路径名转换为路径名字符串
public boolean exists() 测试此抽象路径名表示的File是否存在
public long lastModified() 返回文件最后修改的时间毫秒值
public long length() 返回文件大小,utf-8中英文字母1个字节,汉字3个字节
public File[] listFiles()(常用) 获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回
public boolean mkdir() 只能创建一级文件夹
public boolean mkdirs() 可以创建多级文件夹
public boolean delete() 删除由此抽象路径名表示的文件或空文件夹,默认不能删除非空文件夹
//判断文件是否存在,存在则删除
@Test
public void m(){
    String filePath = "D:\\JetBrains\\hello2.txt";
    File file = new File(filePath);
    if (file.exists()){
        if (file.delete()){  //删除成功返回true
            System.out.println(filePath + "删除成功");
        } else {
            System.out.println(filePath + "删除失败");
        }
    } else {
        System.out.println("该文件不存在");
    }
}
//判断目录是否存在,存在则删除
@Test
public void m2(){
    String filePath = "D:\\JetBrains\\test";
    File file = new File(filePath);
    if (file.exists()){
        if (file.delete()){
            System.out.println(filePath + "删除成功");
        } else {
            System.out.println(filePath + "删除失败");
        }
    } else {
        System.out.println("该目录不存在...");
    }
}
//判断目录是否存在,不存在就创建该目录
@Test
public void m3(){
    String dirPath = "D:\\JetBrains\\test";
    File file = new File(dirPath);
    if (file.exists()){
        System.out.println("该目录存在");
    } else {
        if (file.mkdirs()) {  //创建多级目录使用mkdirs,创建一级目录使用mkdir
            System.out.println("目录创建成功");
        } else {
            System.out.println("目录创建失败");
        }
    }
}

I/O流原理和分类

流与文件:文件通过输入流读入到程序(内存)中,程序输出的结果通过输出流写入到文件里?

  • 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
  • 按数据流的流向不同分为:输入流,输出流
  • 按流的角色的不同分为:节点流,处理流

Java学习笔记

image-20220531102006140

字节流

FileInputStream

image-20220530174632645

该流用于从文件读取数据,它的对象可以用关键字 new 来创建。

可以使用字符串类型的文件名来创建一个输入流对象来读取文件:

InputStream f = new FileInputStream("C:/java/hello");

也可以使用一个文件对象来创建一个输入流对象来读取文件。我们首先得使用 File() 方法来创建一个文件对象:

File f = new File("C:/java/hello");
InputStream out = new FileInputStream(f);

创建了InputStream对象,就可以使用下面的方法来读取流或者进行其他的流操作。

序号 方法及描述
1 public void close() throws IOException{} 关闭此文件输入流并释放与此流有关的所有系统资源。抛出IOException异常。
2 protected void finalize()throws IOException {} 这个方法清除与该文件的连接。确保在不再引用文件输入流时调用其 close 方法。抛出IOException异常。
3 public int read(int r)throws IOException{} 这个方法从 InputStream 对象读取指定字节的数据。返回为整数值。返回下一字节数据,如果已经到结尾则返回-1。
4 public int read(byte[] r) throws IOException{} 这个方法从输入流读取r.length长度的字节。返回读取的字节数。如果是文件结尾则返回-1。
5 public int available() throws IOException{} 返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取的字节数。返回一个整数值。
@Test
public void readFile02() {
    String filePath = "D:\\JetBrains\\hello.txt";
    int readData = 0;
    int readLen = 0;
    FileInputStream fileInputStream = null;
    //使用read(byte [] b)读取
    byte[] buffer = new byte[8];  //一次读取八个字节  循环读入,超过八个会从头覆盖之前的数据
    try {
        //创建FileInputStream对象,用于读取文件
        fileInputStream = new FileInputStream(filePath);
        //read()读取一个字节的数据,如果没有输入则不读,读到最后返回-1  效率较低
        /*while ((readData = fileInputStream.read()) != -1) {
                System.out.print((char) readData);
            }*/
        //使用read(byte [] b)读取提高效率,一次读取八个字节  返回实际读取的字节数
        //底层维护一个byte型数组的缓冲区
        while ((readLen = fileInputStream.read(buffer)) != -1) {
            System.out.print(new String(buffer,0,readLen));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //这里要关闭流对象,防止其占用资源
        try {
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
FileOutputStream

该类用来创建一个文件并向文件中写数据。

如果该流在打开文件进行输出前,目标文件不存在,那么该流会创建该文件。

有两个构造方法可以用来创建 FileOutputStream 对象。

使用字符串类型的文件名来创建一个输出流对象:

OutputStream f = new FileOutputStream("C:/java/hello")

也可以使用一个文件对象来创建一个输出流来写文件。我们首先得使用File()方法来创建一个文件对象:

File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f);

创建OutputStream 对象完成后,就可以使用下面的方法来写入流或者进行其他的流操作。

序号 方法及描述
1 public void close() throws IOException{} 关闭此文件输入流并释放与此流有关的所有系统资源。抛出IOException异常。
2 protected void finalize()throws IOException {} 这个方法清除与该文件的连接。确保在不再引用文件输入流时调用其 close 方法。抛出IOException异常。
3 public void write(int w)throws IOException{} 这个方法把指定的字节写到输出流中。
4 public void write(byte[] w) 把指定数组中w.length长度的字节写到OutputStream中。
@Test
public void writeFile(){
    //创建FileOutputStream对象
    FileOutputStream fileOutputStream = null;
    String outputPath = "D:\\JetBrains\\output.txt";
    try {
        //public FileOutputStream(String name, boolean append) 如果append设为true则在上次写后追加而非覆盖
        fileOutputStream = new FileOutputStream(outputPath);
        //得到FileOutputStream对象后可以调用write方法  可以写入一个字节或多个字节
        try {
            String str = "hello123,world!";
            fileOutputStream.write(str.getBytes());
            //继续写的话会从上次写的光标后开始写
            fileOutputStream.write(str.getBytes(),5,3);  //参数依次为byte[]  off  len 
            //最终写入结果为 hello123,world!123  off为5 从索引为5的字符开始写入三个字符长度
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    }finally {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
//例题 拷贝文件(图片)
public class FileCopy {
    public static void main(String[] args) {
        //完成文件拷贝,将F://算法代码截图//1.png拷贝到D://JetBrains//1.png
        //思路分析
        //1.创建文件输入流,将文件读入到程序
        //2.创建文件输出流,将读取到的文件写入指定位置
        //注意读取部分数据就写入,利用循环写入全部
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        String SourcePath = "F://算法代码截图//1.png";
        String destPath = "D://JetBrains//1.png";
        try {
            fileInputStream = new FileInputStream(SourcePath);
            fileOutputStream = new FileOutputStream(destPath);
            //利用字符数组一次读入多个字符
            byte[] buf = new byte[1024];
            int readLen = 0;
            while ((readLen = fileInputStream.read(buf)) != -1) {
                //读取到后写入到目标文件
                fileOutputStream.write(buf,0,readLen);  //必须使用这个write方法  不能直接传入buf 防止最后写入的字符长度大于文件还剩的字符长度
            }
            System.out.println("copy finished");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

字符流

FileReader

FileReader类从InputStreamReader类继承而来。该类按字符读取流中数据。可以通过以下几种构造方法创建需要的对象。

在给定从中读取数据的 File 的情况下创建一个新 FileReader。

FileReader(File file)

在给定从中读取数据的 FileDescriptor 的情况下创建一个新 FileReader。

FileReader(FileDescriptor fd)

在给定从中读取数据的文件名的情况下创建一个新 FileReader。

FileReader(String fileName)

创建FIleReader对象成功后,可以参照以下列表里的方法操作文件。

序号 文件描述
1 public int read() throws IOException 读取单个字符,返回一个int型变量代表读取到的字符
2 public int read(char [] c, int offset, int len) 读取字符到c数组,返回读取到字符的个数
public class fileReader {
    public static void main(String[] args) {
        String path = "D://JetBrains//hello.txt";
        FileReader fileReader = null;
        char[] buf = new char[50];
        int readLen = 0;
        int data = 0;
        try {
            //创建FileReader对象
            fileReader = new FileReader(path);
            //读取文件内容
            //1.单个字符读取
            while ((data = fileReader.read()) != -1) {  //把异常改为IOException
                System.out.print((char) data);
            }
            //2.字符数组读取
            while ((readLen = fileReader.read(buf)) != -1) {  //把异常改为IOException
                System.out.print(new String(buf,0,readLen));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if(fileReader != null){
                try {
                    fileReader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
FileWriter

写完后必须关闭流或者flush才能真正写入文件,否则只有文件里面没有内容,close相当于flush + 关闭文件

FileWriter 类从 OutputStreamWriter 类继承而来。该类按字符向流中写入数据。可以通过以下几种构造方法创建需要的对象。

在给出 File 对象的情况下构造一个 FileWriter 对象。

FileWriter(File file)

在给出 File 对象的情况下构造一个 FileWriter 对象。

FileWriter(File file, boolean append)

参数:

  • file:要写入数据的 File 对象。
  • append:如果 append 参数为 true,则将字节写入文件末尾处,相当于追加信息。如果 append 参数为 false, 则写入文件开始处。
  • 构造与某个文件描述符相关联的 FileWriter 对象。
FileWriter(FileDescriptor fd)

在给出文件名的情况下构造 FileWriter 对象,它具有指示是否挂起写入数据的 boolean 值。

FileWriter(String fileName, boolean append)

创建FileWriter对象成功后,可以参照以下列表里的方法操作文件。

序号 方法描述
1 public void write(int c) throws IOException 写入单个字符c。
2 public void write(char [] c, int offset, int len) 写入字符数组中开始为offset长度为len的某一部分。
3 public void write(String s, int offset, int len) 写入字符串中开始为offset长度为len的某一部分。
public class fileWriter {
    public static void main(String[] args) {
        String path = "D://JetBrains//fileWriter.txt";
        FileWriter fileWriter = null;
        char[] ch = {'a','b','c'};
        try {
            fileWriter = new FileWriter(path);
            //写入单个字符
            fileWriter.write('a');  //其实write(int b) 但是char和int是可以隐式转换的
            //写入字符数组 默认是覆盖写入
            fileWriter.write(ch);
            //写入指定数组的指定部分
            fileWriter.write("svicen".toCharArray(),0,6);
            //写入字符串
            fileWriter.write("心之所向");
            //写入字符串的指定部分
            fileWriter.write("北京上海",2,2);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                fileWriter.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("程序结束");
    }
}

节点流和处理流

  • 节点流:直接操纵数据源,底层流(低级流)从一个特定的数据源读写数据,如FileWriter和FileReader
  • 处理流:也叫包装流,是连接在已存在的流之上,为程序提供更强大的读写功能,比如BufferedReader,BufferedWriter

image-20220531090741349

public class BufferedReader extends Reader {
    private Reader in;
    private char cb[];
    private int nChars, nextChar;
}
//BufferedReader封装了一个属性Reader,该属性可以是任何节点流,可以放FileReader或者CharArrayReader或者StringReader等,   只要是Reader的子类即可,使用更加灵活          ----对应设计模式 修饰器模式

?修饰器模式模拟见 package com.svicen.reader_

BufferedReader
public class BufferedReader_ {
    public static void main(String[] args) throws Exception{
        String path = "D://JetBrains//hello.txt";
        //创建bufferedReader
        BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
        //按行读取,性能较高   当返回null时读取完毕
        String line;
        while ((line = bufferedReader.readLine()) != null){
            System.out.println(line);
        }
        //关闭流,只需要关闭BufferedReader流即可,底层会自动关闭节点流
        bufferedReader.close();
    }
}
//close底层源码
public void close() throws IOException {
    synchronized (lock) {
        if (in == null)
            return;
        try {
            in.close(); //这里的in就是我们传入的FileReader
        } finally {
            in = null;
            cb = null;
        }
    }
}
BufferedWriter
public class BufferedWriter_ {
    public static void main(String[] args) throws IOException {
        String path = "D://JetBrains//bufWriter.txt";
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(path,true));
        bufferedWriter.write("hello,svicen\n");
        bufferedWriter.write("123456",2,3); //写入456
        //关闭外部流(包装流)即可
        bufferedWriter.close();
    }
}
//拷贝文件示例
public class BufferedCopy_ {
    public static void main(String[] args) throws IOException {
        String srcPath = "D://JetBrains//hello.txt";
        String destPath = "D://JetBrains//hello2.txt";
        BufferedReader bufferedReader = null;
        BufferedWriter bufferedWriter = null;
        String line;
        bufferedReader = new BufferedReader(new FileReader(srcPath));
        bufferedWriter = new BufferedWriter(new FileWriter(destPath));
        while((line = bufferedReader.readLine()) != null){
            //每读取一行就写入到目标文件 readline读取一行的内容,不读取换行符
            bufferedWriter.write(line);
            //插入换行符
            bufferedWriter.newLine();
        }
        if(bufferedReader != null){
            bufferedReader.close();
        }
        if(bufferedWriter != null){
            bufferedWriter.close();
        }
    }
}

?注意,BufferedReader和BufferedWriter用于处理字符流,不能操作二进制文件(声音,视频,docx,pdf)

BufferedInputStream
  • 字节处理流

image-20220601130052160

  • 继承父类FilterInputStream的类型为InPutStream(抽象类)的in属性,而BufferedReader本身有私有的类型为Reader的in属性
BufferedOutputStream
  • 同样也是结点处理流
  • 继承父类FilterOutputStream的类型为OutputStream(抽象类)的out属性,BufferedWriter本身有类型为Writer的out属性

image-20220601130651594

//拷贝图片/视频,字节流用于拷贝二进制文件,也可以用来拷贝字符流文件,但是字符处理流不能控制二进制文件
public class BufferedIOStream_ {
    public static void main(String[] args) throws IOException{
        BufferedInputStream bufferedInputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        String srcPath = "D://JetBrains//1.png";
        String destPath = "D://JetBrains//1_bak.png";
        try {
            //这里是因为FileInputStream是InputStream的子类
            bufferedInputStream = new BufferedInputStream(new FileInputStream(srcPath));
            bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destPath));
            //循环读取文件 写入destPath
            byte[] buf = new byte[1024];
            int readLen = 0;
            //读到-1代表读取结束
            while((readLen = bufferedInputStream.read(buf)) != -1){
                bufferedOutputStream.write(buf,0,readLen);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if (bufferedInputStream != null){
                bufferedInputStream.close();
            }
            if (bufferedOutputStream != null){
                bufferedOutputStream.close();
            }
        }
    }
}

对象处理流

ObjectIputStream
  • 字节流

  • 保存文件时可能需要保存 int 10(同时保存数据类型和值),保存Dog ketty 就需要用到对象的处理流

  • 序列化:在保存数据时保存为数据类型+值的形式,要保存的对象必须可序列化

  • 反序列化:恢复数据的值和数据类型

  • 要实现序列化,必须实现以下两个接口其中一个,Serializable和Externalizable,推荐使用Serializable,这是一个标记接口,内部没有任何方法,Externalizable其实也是实现了Serializable接口,实现该接口需要实现它的这两个方法

    public interface Externalizable extends java.io.Serializable {
        void writeExternal(ObjectOutput out) throws IOException;
        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
    }
    

image-20220601182328442

//读取序列化数据
public class ObjectInputStream_ {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String srcPath = "D://JetBrains//Object.txt";
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(srcPath));
        //读取(反序列化)的顺序必须和保存数据(序列化)的顺序一致,否则会出现异常 先读Int再读Boolean
        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());
        System.out.println(ois.readChar());
        System.out.println(ois.readDouble());
        System.out.println(ois.readUTF());
        Object dog = ois.readObject();
        System.out.println("运行类型:" + dog.getClass());  //会抛出类型转换异常 运行类型为Dog 这里用Object接收
        System.out.println("dog信息" + dog);  //底层Object->Dog
//如果想调用Dog类的方法需要向下转型,而且需要将Dog类的定义放在此类可以引用的地方(单独定义Dog类导入或者放在同一个包内)
        Dog dog2 = (Dog) dog;
        System.out.println(dog2.getName());
        ois.close();
    }
}
ObjectOutputStream

image-20220601182524166

//保存序列化数据
public class ObjectOutputStream_ {
    public static void main(String[] args) throws Exception{
        String filePath = "D://JetBrains//Object.txt";  //保存到txt文件中为乱码
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        //序列化数据到目标文件
        oos.writeInt(100); // int 底层 调用包装类Integer 而Integer是实现了Serializable接口的
        oos.writeBoolean(true);//boolean -> Boolead 实现了Serializable接口
        oos.writeChar('a'); //char -> Character 实现了Serializable接口
        //oos.writeChars("svicen");//String 实现了Serializable接口  没有与其对应的readChars方法
        oos.writeDouble(3.6); //double -> Double 实现了Serializable接口
        oos.writeUTF("苏文成"); //String 实现了Serializable接口
        oos.writeObject(new Dog("旺财",10));//由于Dog没有实现Serializable接口  运行会抛出异常
        oos.close();
        System.out.println("序列化数据保存完毕");
    }
}
class Dog implements Serializable {  //Dog必须实现Serializable接口  否则运行会抛出异常
    private String name;
    private int age;
   //序列化的类中最好加上serialVersionUID的属性,为了提高兼容性。比如 类中加入属性后会认为是增加而不是一个新的类
    private static final long serialVersionUID = 1L;
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

注意事项

  • 注意序列化和反序列化的顺序要一致
  • 序列化对象时,对象必须实现了Serializable接口
  • 序列化的类中建议加上serialVersionUID的属性,为了提高兼容性。
  • 序列化对象时,默认将里面所有的属性进行序列化,除了static和transient(暂时的,不可序列化)修饰的属性
  • 序列化对象时,要求里面属性的类型也需要实现序列化接口,一个对象的属性的类型为其他对象
  • 序列化具有可继承性,如果某类已经实现了序列化,则它的所有子类默认也已经实现了序列化接口

标准输入输出流

image-20220601215750615

public static final InputStream in = null; //System.in  可以看出其编译类型为InputStream  标准输入 --> 键盘
//而System.in运行类型为BufferedInputStream --字节流,包装流(处理流)
public static final PrintStream out = null;//System.out 其编译类型为PrintStream          标准输出 --> 显示器
//System.in运行类型也为PrintStream,是FilterOutStream的子类,也是字节流 BufferedOutputStream也是FilterOutStream的子类

image-20220601220330387

转换流

转换流:将字节流转换为字符流。(需要指定编码方式)transformation

InputStreamReader
  • 是处理流,也字符输入流
  • InputStreamReader构造器中可以传入继承了抽象父类InputStream的任意字类,Charset指定编码方式。
  • InputStream为字节流输入流的顶级抽象父类,Reader为字符流的顶级抽象父类

image-20220602132307421

//利用InputStreamReader解决读取到乱码问题(其实要做的就是指定编码方式)
public class InputStreamReader_ {
    public static void main(String[] args) throws IOException {
        //将字节流 FileInPutStream 转为字符流  指定编码 gbk/utf-8
        String path = "D:\\JetBrains\\hello2.txt";
        //1.将 FileInPutStream 转为 InputStreamReader 并指定编码UTF-8  StandardCharsets是一个类,内有各种编码方式
        InputStreamReader isr = new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8);
        //2.把 InputStreamReader 传入字符处理流 BufferedReader
        BufferedReader bufferedReader = new BufferedReader(isr);
        //3.读取
        String data = bufferedReader.readLine();
        System.out.println("读取到内容" + data);
        //4.关闭外层流即可(处理流)
        isr.close();
    }
}
OutputStreamWriter
  • 是处理流,也字符输出流
  • OutputStream为字节流输出流的顶级抽象父类,Writer为字符流输出流的顶级抽象父类

image-20220602132557184

public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "D:\\JetBrains\\swc.txt";
        String charSet = "gbk";
        //利用OutputStreamWriter把字节流FileOutputStream转为了字符流
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath),charSet);
        osw.write("hello,苏文成");
        osw.close();
        System.out.println("按照" + charSet + "保存文件成功");
    }
}

打印流

  • 只有输出流,没有输入流
PrintStream
  • 字节流,是FileOutputStream的子类,不关闭流也可以写入文件
  • 可以打印到显示器,也可以打印到文件,默认打印到标准输出即显示器。
  • PrintStream的构造器,可以传入文件路径实现打印到文件 image-20220603095224356
public static final PrintStream out = null;//System.out 其编译类型为PrintStream
public class PrintStream_ {
    public static void main(String[] args) throws IOException {
        PrintStream out = System.out;
        out.println("1322");
        out.write("hello,svicen".getBytes());
        out.close();
        //修改打印流打印的位置
        /* System类中有一个设置out的方法
        public static void setOut(PrintStream out) {   是一个native方法,就是java调用非java实现的接口,用c++实现的
            checkIO();
            setOut0(out);
        }
        */
        System.setOut(new PrintStream("D:\\JetBrains\\printStream.txt"));
        System.out.println("修改setOut后   System.out.println打印到文件");
    }
}
//println追进去源码,调用print,print又使用了write方法,所以可以直接使用write方法打印
private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }
PrintWriter
  • 字符流,不关闭流就无法写入文件,不关闭流也无法打印到显示器

    public class PrintWriter_ {
        public static void main(String[] args) throws IOException {
            PrintWriter printWriter1 = new PrintWriter(System.out);
            printWriter1.println("123");
            printWriter1.close();  //必须关闭流,否则显示器上也无法显示123
            PrintWriter printWriter = new PrintWriter(new FileWriter("D:\\JetBrains\\printWriter.txt"));
            printWriter.println(123);
            printWriter.close();  //同样,不关闭流的话文件可以创建,但内容为空  close里面调用了implclose方法writerbytes
        }
    }
    void implClose() throws IOException {
            flushLeftoverChar(null, true);
            try {
                for (;;) {
                    CoderResult cr = encoder.flush(bb);
                    if (cr.isUnderflow())
                        break;
                    if (cr.isOverflow()) {
                        assert bb.position() > 0;
                        writeBytes();  //这个方法真正写入数据,
                        continue;
                    }
                    cr.throwException();
                }
                if (bb.position() > 0)
                    writeBytes();
                if (ch != null)
                    ch.close();
                else
                    out.close();
            } catch (IOException x) {
                encoder.reset();
                throw x;
            }
        }
    

Properties

  • 继承了HashTable类,并且实现了Map接口,使用键值对形式来保存数据

  • Properties还可以从xxx.properties文件中,加载数据到Properties对象并进行读取和修改

  • 一般情况下,xxx.properties文件往往为配置文件。

  • 常用方法

    1. setProperty(String key, String value)
      调用 Hashtable 的方法 put。
    2. getProperty(String key)
      用指定的键在此属性列表中搜索属性
    3. getProperty(String key, String defaultValue)
      用指定的键在属性列表中搜索属性。
    4. load(InputStream inStream)
      从输入流中读取属性列表(键和元素对)。
    5. load(Reader reader)
      按简单的面向行的格式从输入字符流中读取属性列表(键和元素对)。
    6. loadFromXML(InputStream in)
      将指定输入流中由 XML 文档所表示的所有属性加载到此属性表中。
    7. store(OutputStream out, String comments)
      以适合使用 load(InputStream) 方法加载到 Properties 表中的格式,将此 Properties 表中的属性列表(键和元素对)写入输出流。
    8. store(Writer writer, String comments)
      以适合使用 load(Reader) 方法的格式,将此 Properties 表中的属性列表(键和元素对)写入输出字符。
    9. storeToXML(OutputStream os, String comment)
      发出一个表示此表中包含的所有属性的 XML 文档。
    10. storeToXML(OutputStream os, String comment, String encoding)
      使用指定的编码发出一个表示此表中包含的所有属性的 XML 文档。
    //读取配置文件内容,利用properties.getProperty(key)
    public class Properties02 {
      public static void main(String[] args) throws IOException {
          // 1.创建Properties对象
          Properties properties = new Properties();
          // 2.加载指定配置文件
          properties.load(new FileReader("src\\mysql.properties"));
          // 3.把K-V显示到控制台(System.out)
          properties.list(System.out);
          // 4.根据键获取对应的值
          String user = properties.getProperty("user");
          String pwd = properties.getProperty("pwd");
          System.out.println("用户名为:" + user);
          System.out.println("密码为:" + pwd);
      }
    }
    //创建配置文件,利用properties.setProperty(key,value),然后利用store写入文件,如果没有key就是创建,没有就是修改
    public class Properties03 {
      public static void main(String[] args) throws IOException {
          //利用Properties类来创建配置文件,修改配置文件内容
          Properties properties = new Properties();
          //创建三对键值对
          properties.setProperty("charset","utf8");
          properties.setProperty("name","svicen");
          properties.setProperty("pwd","111");
          //将 K-V存储到文件中
          properties.store(new FileOutputStream("src\\mysql2.properties"),null);
          System.out.println("保存配置文件成功");
      }
    }
    

网络编程

InetAddress

public static void main(String[] args) throws UnknownHostException {
    //1.获取本机的InetAddress对象
    InetAddress localHost = InetAddress.getLocalHost();
    System.out.println(localHost); //LAPTOP-V7FVCK3U/192.168.192.1
    InetAddress host1 = InetAddress.getByName("LAPTOP-V7FVCK3U");
    //2.根据指定主机名获取InetAddress对象
    System.out.println("host1 = " + host1);//host1 = LAPTOP-V7FVCK3U/192.168.192.1
    //3.根据域名返回InetAddress对象,比如www.baidu.com
    InetAddress host2 = InetAddress.getByName("www.baidu.com");
    System.out.println("host2 = " + host2); //host2 = www.baidu.com/36.152.44.95
    //4.通过InetAddress对象 获取对应的主机地址
    String hostAddress = host2.getHostAddress();
    System.out.println("host2地址为 " + hostAddress); //host2地址为 36.152.44.95
    //5.通过InetAddress对象 获取对应的主机名/域名
    String hostName = host2.getHostName();
    System.out.println("host2主机名为 " + hostName); //host2主机名为 www.baidu.com
}

Socket

  • Socket意思为插口,通常成为套接字,是客户端与服务端之间进行数据通信的接口

image-20220607215017189

  • 基于Socket编程又分为TCP编程和UDP编程

TCP字节流编程

服务器端:

1.监听本机的 某个 端口

2.当没有客户端连接该端口时,程序会阻塞,等待连接

3.通过socket.getInputStream()获取客户端写入到数据通道的数据,显示

客户端:

1.连接服务端(IP,端口)

2.连接后,通过socket.getOutputStream(),向数据通道写入数据

//服务器端
public class SockcetTCP01Server {
    public static void main(String[] args) throws IOException {
        //1.服务器监听本机9999端口
        ServerSocket serverSocket = new ServerSocket(9999);
        //2.当有客户端连接时,则返回一个socket对象,程序继续执行
        System.out.println("服务端监听9999端口,等待连接...");
        //注意:serverSocket可以通过accept方法,返回多个socket对象(多个客户机连接服务器的多并发)
        Socket socket = serverSocket.accept();
        System.out.println("成功获取socket对象,服务器socket = " + socket);
        //3.通过socket.getInputStream()读取客户端写入到数据通道的数据
        InputStream inputStream = socket.getInputStream();
        //4.IO读取 字节流
        byte[] buf = new byte[1024];
        int readlen = 0;
        while((readlen = inputStream.read(buf)) != -1){
            System.out.println(new String(buf,0,readlen)); //根据读取到的实际长度显示数据
            //public String(byte bytes[], int offset, int length)
        }
        //5.关闭流和socket
        inputStream.close();
        socket.close();
        System.out.println("服务器端退出");
    }
}
//客户端
public class SockcetTCP01Client {
    public static void main(String[] args) throws IOException {
        //1.连接服务器(ip + 端口)  由于服务器,客户机都是本机  连接成功的话返回socket对象
        Socket socket = new Socket(InetAddress.getLocalHost(),9999);
        System.out.println("客户端  socket = " + socket);
        //2.连接后,通过socket.getOutputStream() 获取和socket对象相关联的输出流对象
        OutputStream outputStream = socket.getOutputStream();
        //3.通过输出流对象,向数据通道写入数据
        outputStream.write("hello,server".getBytes(StandardCharsets.UTF_8));
        //4.关闭socket和流对象,必须关闭,否则占用资源
        outputStream.close();
        socket.close();
        System.out.println("客户端断开连接");
    }
}
//例题:客户端向服务器端发送图片,客户端需要IO读磁盘,然后在写入数据通道,服务端需要读数据通道,然后IO写磁盘
//工具类
public class StreamUtils {
    //功能:把输入流转换成byte[]
    public static byte[] streamToByteArray(InputStream is) throws IOException {
        //创建输出流对象
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] bytes = new byte[1024];
        int len;
        while((len = is.read(bytes)) != -1) {
            baos.write(bytes,0,len);
        }
        byte[] arr = baos.toByteArray(); //将输出流读入的内容转为byte数组
        baos.close();
        return arr;
    }
    //功能:将InputStream转换成String
    public static String streamToString(InputStream is) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
        //字符串处理流
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(line + "\r\n");
        }
        return stringBuilder.toString();
    }
}
public class TCPFileUploadClient {
    public static void main(String[] args) throws IOException {
        //客户端向服务器端上传一张图片,并接收服务器端回复的 消息
        //客户端连接服务器端
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        //从磁盘上读取文件 ,利用字节流
        String path = "D:\\JetBrains\\1.png";
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
        //bytes就是这个文件对应的字节数组   BufferedInputStream是InputStream的子类,所以
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //获取输出流,将bytes数据发送到数据通道
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        bos.write(bytes);
        //设置结束标志
        socket.shutdownOutput();
        //获取输入流,读取服务器回送消息 可字节流,也可字符流,这里用了字节流
        InputStream inputStream = socket.getInputStream();
        //使用StreamUtils工具类的方法将输入流转为字符串
        String s = StreamUtils.streamToString(inputStream);
        System.out.println("收到服务器端回送消息: " + s);
        //关闭流
        bis.close();
        bos.close();
        socket.close();
    }
}
public class TCPFileUploadServer {
    public static void main(String[] args) throws IOException {
        //服务器端在本机监听8888端口
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务器端监听8888端口 ");
        //等待客户端连接 ,连接成功后返回一个socket对象
        Socket socket = serverSocket.accept();
        //读取数据通道的内容 先创建输入流对象,然后再读取
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //将得到的bytes数组写入指定路径
        String destPath = "src\\code.png";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath));
        bos.write(bytes);
        bos.close();
        //服务器端向客户端回送消息, 说明收到照片 ,这次用字符流(利用转换流将字节流转换为字符流)
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("收到图片");
        //需要刷新才可写入到数据通道
        bw.flush();
        socket.shutdownOutput();
        //关闭其它资源
        bis.close();
        socket.close();
        serverSocket.close();
    }
}

UDP字节流编程

类DatagramSocket和DatagramPacket(数据包/数据报)实现了基于UDP的协议网络程序

UDP数据报通过数据报套接字DatagramSocket 发送和接收,发送数据前先要建立数据报DatagramPacket对象,系统不保证UDP数据报一定可以安全送到目的地,也不确定什么时候送达

DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号

UDP协议中每个数据报都给出了完整的地址信息,因此无需建立发送方和接收方的连接。

UDP中没有客户端和服务器端的说法,只区分发送端和接收端

image-20220609095833873

//接收端和发送端是相对的,发送端也可以接收数据  两方进行udp通信,只需创建一个datagramSocket对象,可能创建多个packet对象
public class UDPSenderB {
    public static void main(String[] args) throws IOException {
        //1.创建 DatagramSocket 对象  端口最好不与接收端的一样,是发送端要接收数据的端口(不需要连接到接收端)
        DatagramSocket datagramSocket = new DatagramSocket(9999);
        //2.创建一个字节数组, DatagramPacket 对象,需要知道目的主机IP,目的主机端口,用于在网络上传输数据
        byte[] bytes = "hello udp".getBytes(StandardCharsets.UTF_8);  //要发送的数据
        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.192.1"),8888);
        //3.发送数据
        datagramSocket.send(datagramPacket);
        //4.接收数据
        byte[] data = new byte[64 * 1024];
        //发送和接收时datagramSocket是一样的,但是发送时datagramPacket需指明目标IP和端口
        DatagramPacket datagramPacket1 = new DatagramPacket(data, data.length);
        System.out.println("等待接受A回送的消息");
        datagramSocket.receive(datagramPacket1);
        int length = datagramPacket1.getLength();
        byte[] data1 = datagramPacket1.getData();
        String s = new String(data1, 0, length);
        System.out.println(s);
        //5.关闭资源
        datagramSocket.close();
        System.out.println("B端退出");
    }
}
public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
        //1.创建一个 DatagramSocket 对象,端口8888,准备接收数据
        DatagramSocket datagramSocket = new DatagramSocket(8888);
        //2.创建一个 DatagramPacket 对象,传入一个byte数组,准备接收数据
        byte[] buf = new byte[64 * 1024];  //UDP数据包最大 64k
        DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length);
        //3.准备接收数据,调用 datagramSocket 的方法,将通过网络传输的 datagramPacket 填充到 datagramSocket中
        // 有数据包发送给本机的8888端口就会接收到数据,否则阻塞等待
        System.out.println("接收端A,等待接收数据");
        datagramSocket.receive(datagramPacket);
        //4.把 datagramPacket 进行拆包,取出数据并显示
        int length = datagramPacket.getLength();  //实际接收到的数据字节长度
        byte[] data = datagramPacket.getData();
        String s = new String(data, 0, length);
        //5.发送数据,这里实现接收端向发送端发送数据(发送端和接收端都是相对的)
        byte[] sendData = "hello,see you later".getBytes(StandardCharsets.UTF_8);
        //发送数据时 DatagramPacket 的构造器有四个参数,要发送的数据的字节数组,长度,目标IP和端口
        DatagramPacket datagramPacket1 = new DatagramPacket(sendData, sendData.length, InetAddress.getByName("192.168.192.1"),9999);
        datagramSocket.send(datagramPacket1);
        System.out.println(s);
        //6.关闭资源
        datagramSocket.close();
        System.out.println("A端退出");
    }
}
//TCP编程作业
public class Homework01Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        //获取输出流对象
        OutputStream outputStream = socket.getOutputStream();
        //使用字符流处理流写入数据通道
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        //从键盘读取用户输入
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入你的问题");
        String question = scanner.next();
        //将问题写入数据通道
        bufferedWriter.write(question);
        //设置读入结束符  这里要求读取的时候必须使用 readLine()
        bufferedWriter.newLine();
        bufferedWriter.flush();  //字符流写入,需要刷新
        InputStream inputStream = socket.getInputStream();
        //直接利用字节流读入
        int readLen = 0;
        byte[] buf = new byte[1024];
        while((readLen = inputStream.read(buf)) != -1){
            //显示从服务器端读取的数据
            System.out.println(new String(buf,0,readLen));
        }
        /*BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s = bufferedReader.readLine();
        System.out.println(s);*/         //也可以这样读入  利用转换流将字节流转为字符流
        //bufferedReader.close();
        inputStream.close();
        outputStream.close();
        socket.close();
    }
}
public class Homework01Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服务端等待连接");
        Socket socket = serverSocket.accept();
        //连接后成功获取到socket对象
        //获取写入数据通道的输入流
        InputStream inputStream = socket.getInputStream();
        // 读取数据,使用字符流,利用转换流将字节流转换为字符流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s = bufferedReader.readLine();
        String answer;
        if("name".equals(s)) {
            answer = "svicen";
        } else if ("hobby".equals(s)) {
            answer = "programing";
        }else {
            answer = "what?";
        }
        //向数据通道写入数据  这次以字节流的形式
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(answer.getBytes(StandardCharsets.UTF_8));
        socket.shutdownOutput();  //设置结束标志
        /*BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(answer);
        bufferedWriter.newLine();
        bufferedWriter.flush();*/  // 利用转换流将字节流转换为字符流再写入,注意字符流写入需要flush
        //关闭流
        outputStream.close();
        //bufferedWriter.close();
        bufferedReader.close();
        socket.close();
    }
}

反射

应用场景:学习框架时用到的较多

通过外部配置文件,在不修改源码的情况下来控制程序,符合设计模式的ocp原则(开闭原则)

反射其实就是 new 的第二种和形态

package com.svicen.reflection;
import com.svicen.Cat;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;
/**
 * @author svicen
 * @version 1.0
 */
public class ReflectionQuestion {
    // 使用发射类机制 解决通过配置文件加载类
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        // 1.使用 Properties 类可以读写配置文件
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classfullpath = " + classfullpath);
        System.out.println("method = " + methodName);
        // 但是此时的 classfullpath 是一个字符串类型 无法直接创建 Cat对象
        // 2.使用反射机制解决上述问题  可以不修改源码只修改配置文件实现功能的切换
        // (1) 加载类,返回一个 Class 对象,这个类名就叫 Class
        Class<?> cls = Class.forName(classfullpath);
        // (2) 通过 cls 可以得到加载的类的实例对象
        Object o = cls.newInstance();
        System.out.println(o.getClass()); // class com.svicen.Cat
        // (3) 通过 cls 得到加载的类 com.svicen.Cat 的 methodName 的方法对象 "hi"
        Method method1 = cls.getMethod(methodName);
        // (4) 通过 method1 调用方法:即通过方法对象实现调用方法  -- 万物皆对象
        System.out.println("=======反射======");
        method1.invoke(o);  // 反射机制: 方法.invoke(对象)
// 通过 Field 对象获取某个类的成员变量
        Field nameField = cls.getField("name");  // getField 不能得到私有的属性
        System.out.println(nameField.get(o));    //输出:招财猫   反射的 括号里的参数 一般都是对象
    }
}

Java学习笔记

反射的优点和缺点:

优点:可以动态的创建和使用对象(是框架的底层核心),使用灵活

缺点:使用反射机制基本是解释执行,会降低执行速度,可以关闭反射调用方法时的访问检测来优化反射,但效果有限

Class类

  • Class类其实也就是一个普通的类,

  • Class类不可以通过new来实例化对象,而是系统通过类加载器来创建的

            // (1) 传统new对象
            //  底层调用了 loadClass 方法来实例化对象
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return loadClass(name, false);
            }
            Cat cat = new Cat();  // 通过 ClassLoader类 实现类的加载
            // (2)反射方式  注意要将上面的注释掉  否则Cat类已经加载完成了
            Class<?> cls1 = Class.forName("com.svicen.Cat");
            //  同样调用了 loadClass 方法来实例化对象
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return loadClass(name, false);
            }
            // 由于类只加载一次  所以下面输出的 hashCode 是一样的  
            Class<?> cls2 = Class.forName("com.svicen.Cat");
            System.out.println(cls1.hashCode());
            System.out.println(cls2.hashCode());
    
  • 对于某个类的Class对象,在内存中只有一份,因为类只加载一次 多次加载所得的对象的hashCode值一样

  • 每个类的实例都会记得自己是由哪个Class实例所生成,

  • 通过Class对象可以完整地得到一个类的完整结构,通过一系列API

    Java学习笔记

  • Class对象是存放在堆中的

  • 类的字节码二进制数据是存放在方法区的,有的地方称为类的原数据(包括方法代码、变量名、方法名、访问权限等)

Java学习笔记

Java程序的执行阶段

Java学习笔记

  • 获取Class类对象的方式

    • 编译阶段通过 Class.forname()使用,多用于读取配置文件中已知的一个类的全类名
    • 加载阶段通过 类.class ,一般用于参数传递
    • 运行阶段通过 对象.getClass() , 前提是已经有了某类的对象实例
    // 1.Class.forName()
            String classAllPath = "com.svicen.Car";  //一般是读取的配置文件的信息
            Class<?> cls1 = Class.forName(classAllPath);
            System.out.println(cls1);
            // 2.类名.class 一般用于参数传递
            Class cls2 = Car.class;
            System.out.println(cls2);
            // 3.对象.getClass()  前提是已知某类的对象实例
            Car car = new Car();
            Class<? extends Car> cls3 = car.getClass();
            System.out.println(cls3);
            // 4.通过类加载器【4种】
            // (1)先得到类加载器
            ClassLoader classLoader = car.getClass().getClassLoader();
            // (2)通过类加载器获取对象
            Class<?> cls4 = classLoader.loadClass(classAllPath);
            System.out.println(cls4);
            // cls1 cls2 cls3 cls4 实际上是一个对象,hashCode值都是一样的
            // 5.基本数据类型.class
            Class<Integer> integerClass = int.class;
            Class<Character> characterClass = char.class;
            // 6.基本数据类型对应的包装类.TYPE
            Class<Integer> type = Integer.TYPE;
            System.out.println(type);
    
  • 哪些类型有class对象

    Java学习笔记

     Class<String> cls1 = String.class;  // 外部类
     Class<Serializable> cls2 = Serializable.class; // 接口
     Class<int[]> cls3 = int[].class;   // 数组
     Class<Deprecated> cls4 = Deprecated.class;  // 注解
     Class<Thread.State> cls5 = Thread.State.class;  // 枚举
     Class<Long> cls6 = long.class;   // 基本数据类型
     Class<Integer> cls7 = Integer.class;  // 包装类
     Class<Void> cls8 = void.class;  // void 也有
     Class<Class> cls9 = Class.class;  // Class 本事也是一种数据类型  也是外部类
    

类加载过程

Java学习笔记Java学习笔记

Java学习笔记

静态加载

在编译时进行类的加载,依赖性较强。找不到类编译时就会报错

import java.lang.reflect.Method;
import java.util.Scanner;
public class ClassLoad_ {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入key");
        String key = scanner.next();
        switch (key) {
            case "1":
                //Dog dog = new Dog();  // 静态加载类  依赖性较强
                //dog.cry();
                break;
            case "2":
                // 动态加载类  使用时再加载 用到的类
                Class cls = Class.forName("Person");
                Object o = cls.newInstance();
                Method m = cls.getMethod("hi");
                m.invoke(o);
                System.out.println("ok");
            default:
                System.out.println("nothing!");
        }
    }
}
class Dog {
    public void cry() {
        System.out.println("小狗汪汪叫");
    }
}
动态加载

执行到具体的代码时才会加载指定类

加载阶段
  • JVM在该阶段的主要目的是将字节码从不同的数据源(可能是Class文件,jar包,网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象
连接阶段
验证
  • 目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求并且不会危害虚拟机的自身安全
  • 验证包括:文件格式(是否以魔数0xcafebabe开头)、元数据验证、字节码验证、符号引用验证
  • 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间
准备
  • 对于实例属性变量,在准备阶段不会分配内存(类内非静态变量)
  • 对于静态变量,在准备阶段默认初始化为0,在初始化阶段才会初始化为设定的值
  • 对于 static final修饰的静态常量,它一旦赋值后就不再变化,在准备阶段初始化的值即为设定的值
解析
  • JVM虚拟机将常量池的符号引用替换为直接引用(内存地址)的过程
初始化
  • 初始化阶段才真正执行类中定义的 Java程序代码
  • 此阶段是执行<clinit>()方法的过程,该方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并(合并为一个代码块)
  • 虚拟机会保证一个类的 <clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕

获取类结构信息的API

package com.svicen.reflection;
import org.junit.jupiter.api.Test;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
 * @author svicen
 * @version 1.0
 * 演示如何通过反射获取类的结构信息
 */
public class ReflectionUtils {
    public static void main(String[] args) {
    }
    // 第一组 API
    @Test
    public void api_01() throws ClassNotFoundException {
        // 获取 Class 对象
        Class<?> personCls = Class.forName("com.svicen.reflection.Person");
        // getName 获取全类名
        System.out.println(personCls.getName());  // com.svicen.reflection.Person
        // getSimpleName 获取简单类名
        System.out.println(personCls.getSimpleName()); //Person
        // getFields 获取所有public修饰的属性,包括本类以及父类
        Field[] fields = personCls.getFields();
        for(Field filed : fields){
            System.out.println("本类及父类的public属性:" + filed.getName()); // name  hobby
        }
        // getDeclaredFields 获取本类中所有属性R
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println("本类中所有属性:" + declaredField.getName());
        }
        // getMethods 获取本类及父类(超类)中的public修饰的方法
        Method[] methods = personCls.getMethods();
        for (Method method : methods) {
            System.out.println("本类及父类的public方法:" + method.getName()); // 还有超类的方法
        }
        // getDeclaredMethods 获取本类所有方法
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println("本类中所有方法:" + declaredMethod.getName());
        }
        // getConstructors 获取本类所有public修饰的构造器  没有父类
        Constructor<?>[] constructors = personCls.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println("本类所有public修饰的构造器:" + constructor.getName());
        }
        // getDeclaredConstructors 获取本类中所有构造器
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本类中所有构造器" + declaredConstructor.getName());
        }
        // getPackage 以 Package 形式返回包信息
        Package aPackage = personCls.getPackage();
        System.out.println(aPackage);
        // getSuperclass 以Class 形式返回父类信息
        Class<?> superclass = personCls.getSuperclass();
        System.out.println("父类类名:" + superclass.getSimpleName()); // A
        // getInterfaces 以 Class[]形式返回接口信息
        Class<?>[] interfaces = personCls.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println("接口信息:" + anInterface.getName());
        }
        // getAnnotations 以 Annotation[] 的形式返回注解信息
        Annotation[] annotations = personCls.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("注解信息:" + annotation);
        }
    }
}
class  A{
    public String hobby;
    public A(){}
    public void hi(){}
    private void hello(){}
}
interface IA{
}
interface IB{
}
@Deprecated
class Person extends A implements IA, IB{
    // 属性
    public String name;
    protected int age;
    String job;
    private double sal;
    // 构造器
    public Person(){}
    public Person(String name){}
    private Person(String name, int age){}
    // 方法
    public void m1(){}
    protected void m2(){}
    void m3(){}
    private void m4(){}
}
  • getModifiers以int形式返回邢师傅,修饰符对应的值: 0位默认,1为public,2为private,4为protected,8为static,16为final
// 第二组API
    @Test
    public void api_02() throws ClassNotFoundException {
        // 获取 Class 对象
        Class<?> personCls = Class.forName("com.svicen.reflection.Person");
        // getDeclaredFields 获取本类中所有属性
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field declaredField  declaredFields) {
            System.out.println("本类中所有属性:" + declaredField.getName() +
                    "  该属性的修饰符值为:" + declaredField.getModifiers() +
                    "  该属性的类型为:" + declaredField.getType()); // 返回属性对应的类的 Class 对象
        // 修饰符对应的值: 0位默认,1为public,2为private,4为protected,8为static,16为final
        }
        // getDeclaredMethods 获取本类所有方法
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println("本类中所有方法:" + declaredMethod.getName() +
                    "  该方法的访问修饰符的值:" + declaredMethod.getModifiers() +
                    "  该方法返回类型:" + declaredMethod.getReturnType());  // 返回返回类型对应的Class对象
            // 输出方法的形参类型
            Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println("该方法的形参类型:" + parameterType);
            }
        }
        // getDeclaredConstructors 获取本类中所有构造器
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本类中所有构造器" + declaredConstructor.getName());
            // 获取构造器的形参类型
            Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println("该构造器的形参有:" + parameterType);
            }
            System.out.println("============================");
        }
    }    

反射爆破创建实例

  • 反射爆破,可以使类的访问修饰符无效,通过设置setAccessible为true
public class ReflecCreateInstance {
    public static void main(String[] args) throws Exception {
        //1.先获取到User类的Class对象
        Class<?> userClass = Class.forName("com.svicen.reflection.User");
        //2.通过public的无参构造器创建实例
        Object o = userClass.newInstance();
        System.out.println(o);
        //3.通过public的有参构造器创建实例
        // 首先获取有参构造器
        Constructor<?> constructor = userClass.getConstructor(String.class);
        Object swc = constructor.newInstance("苏文成");//体现反射 方法(对象)
        System.out.println(swc);
        //4.通过非public的有参构造器创建实例
        Constructor<?> declaredConstructor = userClass.getDeclaredConstructor(int.class, String.class);
        // 爆破:使用反射可以利用私有的构造器构造实例
        declaredConstructor.setAccessible(true);  // 设置爆破,否则对于非public的构造器创建实例会报错
        Object swc1 = declaredConstructor.newInstance(21, "swc"); // 由于构造器是私有的  会报错
        System.out.println(swc1);
    }
}
class User{
    private int age = 20;
    private String name = "svicen";
    public User(){}
    public User(String name) {
        this.name = name;
    }
    private User(int age, String name) {
        this.age = age;
        this.name = name;
    }
    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

反射爆破操作属性

  • 对于static属性,set方法第一个参数可以设置为null,也可以设置为具体的对象
public class ReflecAccessProperty {
    public static void main(String[] args) throws Exception{
        //1. 获取Student类对应的Class对象
        Class<?> stuClass = Class.forName("com.svicen.reflection.Student");
        //2. 利用无参构造器创建对象
        Object o = stuClass.newInstance();
        System.out.println(o);
        //3. 使用反射得到 age 属性对象
        Field age = stuClass.getField("age");
        age.set(o,22);  //通过反射对指定对象设置属性值
        System.out.println(o);
        System.out.println(age.get(o));  // age.get() 括号内传入对象
        //4. 使用反射操作 私有的、静态的 name对象
        Field name = stuClass.getDeclaredField("name");
        name.setAccessible(true);
        name.set(null, "svicen");
        System.out.println(o);
        System.out.println(name.get(null));
    }
}
class Student{
    public int age;
    private static String name;
    public Student(){}
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name=" + name + '}';
    }
}

反射爆破操作方法

public class ReflecAccessMethod {
    public static void main(String[] args) throws Exception {
        // 1.得到 Boss 类对应的Class对象
        Class<?> bossCls = Class.forName("com.svicen.reflection.Boss");
        // 2.创建一个 对象
        Object o = bossCls.newInstance();
        // 3.得到public的hi方法对象  注意需要指明参数的类型对应的class对象
        Method hi = bossCls.getMethod("hi", String.class);
        // 4.通过反射调用该方法
        hi.invoke(o, "svicen");
        // 5.private且static的say方法对象
        Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
        // 6.通过反射调用方法  注意invoke的第一个参数可以为null,因为是静态方法
        say.setAccessible(true);  // 因为方法时私有的,所以需要爆破
        System.out.println(say.invoke(null,22,"svicen",'男'));
        // 如果调用的方法有返回值,对于反射而言,返回的类型一定为 Object
        // 但是运行类型和方法中定义的方法类型一致
        Object returnVal = say.invoke(null, 33, "sv", '女');
        System.out.println(returnVal.getClass());
    }
}
class Boss{
    public int age;
    private static String name;
    public Boss(){}
    private static String say(int n, String s, char c) {
        return n + " " + s + " " + c;
    }
    public void hi(String s){
        System.out.println("hi " + s);
    }
}
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。