Java笔记

目录

IDEA快捷键

JDK JRE JVM三者之间的关系之间的关系

基础

变量

取值范围

整形数

short byte char混合运算

浮点型

switch语句

break语句

实参

break和return

缺少返回语句

方法重载

JVM内存结构

空指针

构造方法

static

实例语句块

this

toString()

方法覆盖override

多态

instanceof

super

final

abstract

接口

lang包

访问控制修饰符

Object里的常用方法

equals方法

finalize方法

hashCode方法

内部类

匿名内部类

数组

一维数组初始化

方法参数是数组

main方法中的参数

数组的扩容

二分法查找

String类

存储原理

构造方法

常用方法

StringBuffer和StringBuilder

StringBuffer和StringBuilder区别

String类中唯一静态方法valueOf()

包装类

Integer

int String Integer相互转换

日期类

数字类

DecimalFormat

BigDecimal

枚举

异常

try…catch

java8新特性

异常中常用方法

finally语句

 自定义异常

集合

Collection集合

List集合

ArrayList

LinkedList

Vector

泛型

 自定义泛型

Map集合

HashMap

HashTable

Propreties


IDEA快捷键

Ctrl+Shift+F10 运行程序

Fn+Home 光标移动到行首

Fn+End 光标移动到行尾

Ctrl+Alt+V 自动补全变量

Ctrl+D 向下复制一行

Ctrl+F12 在类中查找方法

Shift 连按2下 查找类

Ctrl+F 查找输入字符串出现位置

Ctrl+Y 删除光标所在的一行

Alt+鼠标向下拖 编辑多行

Ctrl+P 查看方法参数

JDK JRE JVM三者之间的关系之间的关系


 JDK是Java开发工具箱,JRE是Java运行环境,JVM是Java虚拟机。
JDK包括JRE, JRE包括JVM。

JDk是Java的development kit,Java开发工具箱。
JVM是不能独立安装的,但是JRE和JDK都是可以独立安装。
安装JDK的时候, JRE就自动安装了。同时,JRE内部的JVM也自动安装了。

问题
假设你在软件公司开发了一个新的软件。
现在要去客户那边要去客户那边给客户把项目部署一下,需要安装jdk吗?
假如说我现在在北京,我开发了一个软件,给武汉部署。这个时候呢,只需要安装JRE就行了。
JRE体积很小,安装非常便捷
你又不在这个武汉的这个客户机器上开发。
所以你不需要安装JDK

为什么安装JDK的时候会自带一个JRE
因为Java程序员开发完程序之后要测试这个程序,让这个程序运行起来。需要JRE。
----------------------------------------

基础

Windows操作系统需要安装windows版本的Java虚拟机,Linux操作系统上需要安装Linux版本的java虚拟机。
Java虚拟机是不一样的。
操作系统是不一样的。
但是你的JAVA程序是一样的。
你的Java程序是放在java虚拟机。
所以你的java程序压根儿就没有和windows是打交道。
JAVA虚拟机他把操作系统之间的差异给屏蔽掉了。
优点就是咱们跨平台了,对吧?编写一次。
但是缺点是麻烦,就是你你这个程序直接扔到windows里面能运行吗?
运行不了你的这个程序直接扔到Linux里面能运行吗?
运行不了。
你必须得有JVM。
而你要想有jvm,你必须得有jdk。


编译阶段和运行阶段可以在两个不同操作系统嘛?
可以,如windows编译生成的字节码放到linux运行。

字节码是二进制嘛?
不是,如果是就不需要jvm了,操作系统可以直接运行二进制

java命令后面只能跟类名
java HelloWorld
java HelloWorld.class(错误)

类加载器classloader默认从当前目录下找字节码文件。

classpath给类加载器指路,classpath不属于windows操作系统,属于java


变量

变量必须先声明再赋值,才能访问。
int age;
System.out.println(age);
报错未初始化变量age

int a,b,c=100;
a和b没有初始化赋值,只有c=100
----------------------------------------

取值范围

byte -128-127
最大可表示01111111(最左边符号位)
2^7-1
10000000 -1


整形数

数据默认当做int类型,需要存为long在后面加l或L

从小到大自动类型转换
long a =100;

long b=2147483648;(报错)
int最大2147483647

强制类型转换
long a =100L;
int b=(int)a; 因为100在int范围内,精度不会丢失
long类型强转int,会将前4个字节砍掉

short byte char混合运算

各自转换成int再运算

char c1='a';
byte b=1;
System.out.println(c1+b);  //98
//short s=98 编译通过
//short s=c1+b;编译报错,因为加法运算编译阶段不知道结果,只知道结果类型为int
//short s=(short)c1+b 编译报错,运算顺序为short类型的c1+byte类型的b,结果还是int
short s=(short)(c1+b); //编译通过



多种数据类型混合运算,结果为范围最大的

long a =10L;
char c='a';
short s=100;
int i=30;
long x=a+c+s+i;

浮点型

银行/财务系统采用比double精度更高的java.math.Decimal(引用数据类型)

任意一个浮点型表示范围都大于整数型
float容量>long容量

浮点型默认当做double,加f或F表示float


switch语句

1.低版本JDK只支持int,(包括byte short char,存在自动类型转换)。高版本支持String int 枚举
2.如果分支执行,但是分支最后没有break,会出现case穿透现象,即不再比较下面的值,直接执行其他分支的语句,直到break

switch(变量){

   case 值:

      JAVA语句;

      break;

   …

break语句

a:for(k=0;k<2;k++){
    b:for(i=0;i<10;i++){
        if(i==5){
           break a; //跳出指定循环
}
         System.out.println(i);
}
}
//输出结果01234


实参

public static int sum(int a,int b){
  return a+b;
}

byte x=1;
byte y=2;
sum(x,y);  //形参是int类型可以传byte,相当于int a=x;


break和return

break终止switch和离他最近的循环
。 return终止离他最近的方法

缺少返回语句

public static int m(){
  boolean flag=true;
  if(flag){
    return 1;
}
}


编译报错,没有返回语句,因为编译器只知道flag是boolean类型,不知道值是true,编译器会认为flag有可能是false,则return语句不会执行


方法重载

和返回值类型、修饰符无关,和方法名自己形参列表有关。
返回值、修饰符类型不同,方法名相同,参数列表相同,视为方法重复,编译不通过。


JVM内存结构

方法区:类加载器加载class文件到方法区,方法区存的是字节码的存代码片段
堆内存:new出来的对象和对象的实例变量
:方法调用时为方法在栈内存开辟一块空间,存方法里的局部变量

问题:引用一定是局部变量嘛?

不一定。

3dee71fabb644c8b9f039617f807200f.jpg

 栈里的a和u是引用,存储了一个指向对象的内存地址,在main方法里,是局部变量。

堆里的addr也存储了一个指向对象的内存地址,是引用,但它在类体中,方法之外,它属于成员变量或者说实例变量。


成员变量不能通过类名访问,要通过对象。


空指针

"空引用”访问实例变量/成员变量时,发生空指针异常

public class Nullpointer{

  public static void main(String[] args){

    Customer c=new Customer();

    System.out.println(c.id);

    c=null; //空引用

    System.out.println(c.id);  //空指针异常

}

}

class Customer{

  int id;

}


构造方法

问题:实例变量没有手动赋值的时候,系统会赋默认值,是类加载的时候赋值的吗?

不是,类加载在方法区。而实例变量不能通过类名访问,只能创建对象访问。创建对象在堆内存,先在堆内存new一个对象,才会开辟一块内存空间放实例变量。new对象实际上是在调用构造方法,所以实例变量是在构造方法执行的时候初始化、完成赋值的。


static

在方法体内的变量称为局部变量,在方法体外的变量叫成员变量。

成员变量分为实例变量和静态变量。

静态变量和静态方法都是类相关,通过“类名.”访问,不需要new一个对象。

实例变量和实例方法都是对象相关,通过“引用.”访问,需要先new一个对象。

静态变量在类加载时初始化,不用创建对象。静态变量放在方法区

实例的一定要用引用.访问

静态的建议用类名.访问,也可以用引用.访问

** 空引用.静态变量不会发生空指针异常。

static静态代码块

类加载时执行,且只执行一次。

一个类中可以编辑多个静态代码块。

public class StaticTest {
    static int a,b,c;
    static {
        a=10;
        System.out.println("静态代码块1执行了");
    }

    public static void main(String[] args) {
        System.out.println(a+b+c);
    }
    static {
        b=100;
        c=200;
        System.out.println("静态代码块2执行了");
    }
}

运行结果:

静态代码块1执行了
静态代码块2执行了
310 


实例语句块

实例语句块在构造方法之前执行。

public class InstanceTest {
    int count;

    public InstanceTest() {
        System.out.println("构造方法执行");
    }

    {
        count=10;
        System.out.println("实例语句块执行了");
    }

    public static void main(String[] args) {
        InstanceTest instanceTest = new InstanceTest();
        System.out.println(instanceTest.count);
    }
}

运行结果:

实例语句块执行了
构造方法执行
10 


this

一个对象,一个this。

this是一个变量,一个引用,存放指向自身的内存地址。

this的代码复用

this(实际参数列表)

用于在同一个类中调用另一个构造方法,且必须是构造器中的第一个语句。

class Date{

  private int year;

  private int month;

  private int day;

  public Date(){

    this(1970,1,1);

}

  public Date(int year,int month,int day){

    this.year=year;

    this.mouth=month;

    this.day=day;

}

}


toString()

System.out.println()里面是引用的时候,默认调用该引用的toString()方法。

public class Test{

  public static void main(String[] args){

   A a=new A();

   System.out.println(a);

 //结果等同于System.out.println(a.toString());

//输出结果:A(类名)@十六进制(经过哈希算法处理的引用的内存地址)

}

}

class A{}


方法覆盖override

1.子类对父类方法进行覆盖时,访问权限只能更高不能更低。比如父类方法使用public,子类就不能使用其他的。

2.重写之后的方法不能比之前的方法抛出更多的异常,可以更少。

3.重写之后的两个方法,拥有相同的返回值类型,相同的方法名,相同的参数列表

** 返回值类型是基本数据类型必须相同。

**返回值类型是引用类型,如Animal是Cat的父类,有一个类x中有一个返回值类型为Animal中的方法,x的子类y重写该方法,返回值类型可以为Cat,但不能为Object

4.私有方法无法覆盖。

5.方法覆盖只针对实例方法,静态方法没有意义

原因:

class Animal{

  public static void move(){

    System.out.println("动物在移动");

}

}

class Cat extends Animal{

  public static void move(){

    System.out.println("猫在跑");

}

}

Animal a=new Cat();  //多态

a.move();
//静态方法用类名.调用,就算使用引用.,实际还是类名.调用的。
//运行结果是“动物在移动”,因为a是Animal类,调用Animal中的move方法。

Cat c=new Cat();

c.move();
//运行结果是“猫在跑”,但这样的方法重写没有意义,跟在Cat类中另外定义一个新方法效果一样。


多态

多态是指编译的时候一种形态,运行时另一种形态。

向上转型:子--父

向下转型:父--子

class Animal{

  public void move(){

    System.out println("动物在移动");

}

}

class Cat extends Animal{

  public void move(){

    System.out println("猫在跑");

}

}

Animal a=new Cat();  //多态

编译时,编译器识别到引用a的类型是Animal,静态绑定Animal里的move()方法。

运行时,创建的对象实际是Cat的对象,所以动态绑定Cat中的move()方法。

instanceof

instanceof可以在运行阶段动态判断引用指向对象的类型。

语法格式

(引用 instanceof 类型)

执行结果为boolean

向下转型时使用instanceof运算符进行判断,可以避免ClassCastEception类型转换异常


super

8faeed8c3a354f149ad28256aa309e32.jpg

super()表示在子类的构造方法中调用父类的构造方法,模拟现实世界中先有父亲才有儿子。

class A{

  public void A(){

    System.out.println("A的无参构造");

}

}

class B extends A{

  public void B(){

    //super(); 不写也有

    System.out.println("B的无参构造");

}

}

new B();

//运行结果:

//A的无参构造

//B的无参构造

原因:B的构造方法中第一行默认有一个super();

如果A类只写了一个有参构造,就不会自动给无参构造方法。如果A有无参构造的时候,B调用无参构造的时候会自动写一行super(),可以不写,默认会有。但A只有有参构造,B的有参构造或者无参构造都必须手动写super(参数)。

class A{

  public void A(int i){

    System.out.println("A的构造");

}

}

class B extends A{

  public void B(){

    super(1);

    System.out.println("B的无参构造");

}

}

super和this不能共存,有this没super,有super没this。如果子类的一个构造方法中调用了this,那么其他的构造方法总会调用super,也就是说子类调用构造方法,就一定会先调用父类的构造方法。

super调用父类的构造方法,不是创建父类的对象,只是继承了父类的特征。

"super."什么时候不能省略?

子类有和父类同名的属性,并且在子类中想访问父类的那个属性时,super.不能省略。

super不是引用,不保存内存地址,也不指向对象。super只代表当前对象内部的那一块父类特征。

super.方法(实参)代表调用父类方法。


final

final修饰的类无法被继承,final修饰的方法不能被覆盖,final修饰变量只能赋值一次。final修饰的实例变量系统不会赋默认值,必须手动赋值,可以在变量后面等号赋值,也可以在构造方法里赋值。(final修饰的实例变量没有手动赋值会编译报错未初始化,一般实例变量没有赋值,系统会赋初始值,初始化是在构造方法调用时,即new一个对象的时候,构造方法里什么都不写也会默认有一个赋初始值,但final修饰的实例变量系统不管。)

final和static联合修饰的变量叫常量,通常全部字母大写,单词之间下划线连接。


abstract

抽象类无法实例化,不能创建对象。

abstract和final不能联合使用,因为final修饰的类不能被继承,abstract修饰的类就是来被继承的。

抽象类有构造方法,是供子类创建对象用的。因为子类调用构造方法时会先调用父类构造方法(super())。

抽象方法以分号结尾,没有方法体,没有具体的实现。

抽象类中可以没有抽象方法,但抽象方法必须在抽象类中。父类是抽象类,父类中有一个抽象方法,子类要继承父类的抽象方法时,两种方式:1.子类也是抽象类,重写父类的抽象方法,抽象类中可以有抽象方法。2.子类是非抽象类,重写父类的抽象方法时变成非抽象方法。

判断:没有方法体的方法都是抽象方法。

错误。Object类中就有很多没有方法体的方法,但是不是抽象方法,没有abstract关键字修饰,但有native修饰,代表调用c++代码。


接口

接口只能用public修饰,但public不是必须写,可以省略。

接口也是一种引用数据类型,编译生成.class

接口可以继承,并且支持多继承。一个接口可以继承多个接口。

接口里只有常量和抽象方法。

接口里所有的元素都是public修饰。

接口里的抽象方法,public abstract可以省略。

接口里的常量,public static final可以省略,随意写一个变量int i=1;都是常量,会自动在前面加上public static final。

接口不能实例化,但是可以用多态。

如:

public Interface A{}

public class B implements A{}

public class Test{

  public static void main(String[] args){

      //可以看做继承

       A a=new B();

}

}

在编译阶段,接口和接口做强制类型转换时,以及类强转成接口时,不是继承关系也不会报错。但是运行阶段可能出现ClassCastEception。运行阶段有继承关系才能强转。


lang包

lang包下的直接子类不用导入,会自动导入,如String类。


访问控制修饰符

protected 本类 同包 子类

default 本类 同包

接口和类只能用public和default修饰。


Object里的常用方法

equals方法

Object类里提供的equals比较的是对象的内存地址,想比较对象的内容要自己重写。

基本数据类型判断相同用==,引用数据类型用.equals。String类也是引用数据类型,比较字符串相同要用.equals,String类已经重写了equals方法。因为类都有构造方法,如果用new String("abc")创建的对象用==比较的是内存地址,如果是String s="ab"可以用==,但是不知道创建的方式会是什么,所以都用.equals

be1ef55ca143487e8efaff06f0a3096a.jpg

 前两行为模板

finalize方法

protected修饰

不需要程序员手动调用,JVM的垃圾回收器在回收前自动调用,程序员只需要重写。相当于给程序员准备的一个垃圾回收时机。

注意:垃圾回收器不轻易启动,时间没到,垃圾太少,种种原因,可能不会启动,该方法可能不会被调用。

System.gc()可以建议垃圾回收器启动,但只是建议,不一定启动。

hashCode方法

底层调用c++,返回int类型对象的哈希码,实际上是对象的内存地址经过哈希算法得出来的值。


内部类

在一个类里再定义在一个类

静态内部类,相当于静态变量,用static修饰

实例内部类,相当于实例变量

先实例外面的类才能实例化

new Test().new Inner()

局部内部类,相当于局部变量,定义在方法里的类

匿名内部类

是局部内部类的一种

因为接口不能new对象,所以要创建接口的实现类,再实例化实现类的对象。匿名内部类就是不用再创建实现类,直接new 接口名(){具体实现}。

缺点是只能使用一次。


数组

如果数组存储的是java对象,实际上存的是引用,是内存地址。

数组一旦创建,长度不可变。

数组自带length属性,表示元素个数。

数组中元素的内存地址连续。

数组中第一个元素的内存地址,作为整个数组对象的内存地址。

优点:

查询效率高,因为知道第一个元素的内存地址,每个元素类型相同所以占用空间大小相同,知道下标就是知道偏移量,可以通过数学表达式计算出任意位置的元素的内存地址。

缺点:

1.为了保持内存地址的连续性,随机增加或删除一个元素时,涉及到一系列元素向前或向后移动的操作。

2.不能存储大数据量,因为很难在内存中找到一块特别大的连续内存。

一维数组初始化

静态初始化

int[] arr1={23,88,65}

动态初始化

int[] arr2=new int[5]

方法参数是数组

动态

printArray(new int[5]);

静态

int[] arr={1,2,3};

printArray(arr);

-----------------

 printArray(new int[]{1,2,3});

main方法中的参数

JVM调用main方法自动传过来的数组默认长度为0,也就是数组创建了但里面没存东西。

String[] s1={};

String[] s2=new String[0];

该数组是留给用户的,用户可以从控制台传递参数

class Test03{

  public static void main(String[] args){}

}

例如这样运行

java Test03 AAA bbb ccc

以空格划分,把AAA bbb ccc作为元素添加到args了。

在idea里面run选择edit configuration,在program configuration里面填AAA bbb ccc效果相同。

数组的扩容

创建一个更大的数组,把原来数组拷贝到新数组中。

System.arraycopy(源数组,从几号索引开始拷贝,目标数组,拷贝到新数组的几号索引,拷贝的长度)

二分法查找

Arrays.binarySearch(int 数组,查找的元素)

返回一个int值,为查找元素的下标。没有找到返回一个负数。

作用:查找某个元素的下标。

适用于已经排好序的数组。

(首元素下标+末元素下标)/2=中间元素下标,用中间元素和目标元素比较是否相等,如果不相等。中间元素大于目标元素,(原首元素下标+原中间元素下标-1)/2,再次得到中间元素,与目标元素比较,直到找到目标元素;第一次比较如果中间元素<目标元素,(中间元素下标+1)+末尾元素/2,同上。


String类

因为字符串使用频繁,在字符串常量池中创建可以提高效率。如s1="abc";在常量池中创建了"abc",s2="abc"就不用再次在常量池中创建新对象,s1和s2存储的内存地址相同。

存储原理

凡是双引号“”都会在方法区的字符串常量池里创建对象。ff71628eec0e4662bb0e114a0372f89a.jpg

String s1=“abcdef”

在字符串常量池中创建“abcdef”,s1中保存字符串常量池中对象的内存地址。

String s2="abcdef"+"xy"

已有的"abcdef"可以直接拿来用,在常量池创建"xy",因为拼接操作,创建新的字符串"abcdefxy",s2中保存新字符串内存地址

String s3=new String("xy")

堆中创建String对象,里面存放"xy"的内存地址,s3中存放String对象内存地址。

构造方法

b30c65e764584c488ec46648f5aeda54.jpg

常用方法

char charAt(int index) 输入索引返回对象字符

int compareTo(String s) 字典顺序比较字符串

相等返回0 前小后大返回-1 前大后小返回1

boolean contains(CharSequence s)是否包含子字符串

StringBuffer和StringBuilder

String的底层是final byte[]

StringBuffer底层是byte[]

所以String是不可变的,在需要频繁字符串拼接的时候如果使用String用+拼接,会占用方法区里的大量内存。

使用StringBuffer的append()方法,底层调用了System.arrayCopy()进行数组拷贝,从而实现数组的扩容。

StringBuffer的空参构造方法创建的是byte[16],也可以使用StringBuffer(int capacity)指定容量。如果不指定容量会自动扩容,但是最好一开始定义合适的容量,避免频繁扩容。

StringBuffer和StringBuilder区别

StringBuffer里的方法都是synchronized修饰的,StringBuilder里的方法没有synchronized修饰。

代表StringBuffer是多线程安全的,StringBuilder是多线程不安全的。

String类中唯一静态方法valueOf()

System.out.println()底层调用了String.value Of(),这个方法底层调用了toString(),所以控制台输出的都是字符串。


包装类

基本数据类型不够用了,所以需要包装类。

jdk1.5之前,例如一个方法需要一个Object类型参数,不能传入如int的基本数据类型参数。

装箱:基本数据类型--->引用数据类型

使用new对象的方式把基本数据类型作为构造方法的参数传入可以完成装箱。如Interger i

=new Integer(123);

拆箱:引用数据类型--->基本数据类型

以下为Number类中的方法,Number是一个抽象类,它是所有包装类数字类的父类。

b0868aed6f85461f8307bf67beb91e85.jpg

 int a=i.intValue(); //拆箱

Integer

有两种构造方法,其他包装类相同。

Integer a=new Integer(123);

Integer b=new Integer("123");

访问包装类中的常量获取最大值、最小值

Integer.MAX_VALUE

Integer.MIN_VALUE

jdk1.5之后支持自动装箱和拆箱

Integer a=100; //int类型自动转成Integer

int b=a; //Integer自动转成int

Integer a=100;

Integer b=100;

System.out.println(a==b); //true

Integer a=128;

Integer b=128;

System.out.println(a==b); //false

原因:

Integer用==比较

int String Integer相互转换

5aa3b52fafb746b09d070cab560eae98.jpg

可能出现的异常

int i=Integer.parseInt("123");

Integer.parseInt("中文");

//出现NumberFormatException


日期类

java.util.date

new Date()

new Date(long 毫秒数)

输出Date对象的引用,会自动调用Date重新的toString()输出时间

System.currentTimeMillis()

获取1970年1月1日0时0分0秒至今的毫秒数。

java.text.SimpleDateFormat

SimpleDateFormat sdf=

new SimpleDateFormat("日期格式模板");

日期--->字符串

String s=sdf.format(new Date());

字符串--->日期

Date d=sdf.parse("和日期格式模板对应的日期字符串");


数字类

DecimalFormat

DecimalFormat df=

new DecimalFormat("数字格式化")

# 任意数字

, 千字符

. 小数点

###,###.##      ###,###.0000 保留四位小数

1,234.56


BigDecimal

BigDecimal bd=new BigDecimal(100);

BigDecimal bd1=new BigDecimal(100);

这里不是普通的100,是精度很高的100

bd是一个引用不能直接用运算符如+计算

bd.add(bd2) 加法

bd2.divide(bd)  除法


枚举

枚举是引用数据类型,编译会生成class

枚举值相当于常量

enum 枚举名{

    枚举值,枚举值

}

public class Test01 {
    public static void main(String[] args) {
      Seasons season=Seasons.summer;
      switch (season){//switch(Seasons.summer)
          case spring:
              System.out.println("春天");
              break;
          case summer:
              System.out.println("夏天");
              break;
          case autumn:
              System.out.println("秋天");
              break;
          case winter:
              System.out.println("冬天");
              break;

      }
    }
}

enum Seasons{
    spring,summer,autumn,winter
}

异常

异常在JAVA中以类的形式存在,可以创建异常对象。

我们看到的控制台异常信息,就是在程序发生异常时,JVM创建异常对象后抛出,输出到控制台。

异常的父类是Throwable,Throwable父类是Object。

Throwable下面有两个子类,ErrorException。不管是错误还是异常都是可抛出的。

Error:错误只要发生就会终止程序,退出JVM。错误是不能被处理的。

Exception:有两个分支,一个是直接子类,一个是RuntimeException。直接子类都是编译时异常,RuntimeException是运行时异常。

问题:编译时异常是编译阶段发生的吗

不是,是在编写程序时必须预先处理的,不处理编译报错。

try…catch

catch语句可以有多条

catch的异常从上到下要从小到大

try{

}

catch(IOException){}

catch(FileNotFoundException){}

报错

java8新特性

catch(FileNotFoundException | Nullpointer Exception e){}

异常中常用方法

1.exception.getMessage()

String msg=exception.getMessage();

获取简单异常描述信息

Nullpointer exception e=new NullPointerException("空指针异常");

System.out.println(e.getMessage());

输出的是构造方法里的字符串

2.exception.printStackTrace()

打印异常追踪的堆栈信息

java采用异步线程打印

finally语句

和try一起使用,finally语句中的代码是最后执行的,而且一定会执行,即使try中语句有异常。

作用:完成源的释放/关闭。

因为如果关闭资源的语句放在try中,如果前面有异常,就不会执行后面的语句,也就不会关闭资源,在finally里完成源的释放有保障。

5dfd46a29cd84a3fab12dbe9bebdac3d.jpg

 退出JVM,finally不执行

93d218622cd4489f82c0c7050c376c4e.jpg

面试题

0c2963cf97fd4925b8d96b10d98d3d08.jpg

 自定义异常

SUN定义的异常在之后的业务中可能不够用,可能有一些和业务挂钩的异常需要自定义。

继承Exception或者RuntimeException。

写一个无参构造方法,一个有参构造方法。

注意:子类对父类方法进行覆盖时,只能抛出比父类更少、更小的异常,父类方法没抛异常,子类方法覆盖不能抛异常,但可以抛RuntimeException。

getMessage()为什么可以拿到构造方法里传的字符串?

先自定义一个异常类

de6b7b7305b74519b377f3cc6ea8043c.jpg

 自定义异常时写一个无参构造方法,一个有参构造方法,有参构造的第一行super(s),不写这一行会自动调用父类的无参构造方法。

点进去

s传给了Exception的有参构造

c6c2e780a2a543a8b44bdc602b2e74fc.jpg

 再点进去

afaf26afe0714969bf8dc4d6d8790381.jpg

 继续传给了Throwable的有参构造,并赋值给detailMessage

edab7aba7d1b4569b1a3e43a422f6968.jpg

 detailMessage是一个实例变量

4d50cdf553b446b4a5e00dea07232d8c.jpg

 在Throwable里的getMessage()方法返回了detailMessage的值,也就是有参构造里传入的s


集合

数组既可以存基本数据类型,也可以存对象的内存地址。集合只能存内存地址。

不同的集合底层是不同的数据结构,数据结构是存储数据的方式,如数组、二叉树、链表都是数据结构。

Collection集合

Collection接口的父类是Iterable接口,Iterable接口里有一个iterator()方法,这个方法的返回值类型是一个叫Iterator的接口。

(接口)Iterator it=

Collection将来的子类构造的对象.iterator()

it是一个迭代器

9720e6ca11ef4e1081d7dafa0bc7ae3d.jpg

 Iterator接口里有三个方法,it调用这些方法可以对Collection将来的子类构造的对象进行迭代。

如果使用Collection的方法更改集合,就要创建新的迭代器,否则调用next()方法时会出异常。使用迭代器自己的remove()方法没关系,会删除当前指向的元素。

Collection接口有两个常用的子接口,List接口和Set接口。

  • List接口:有序、可重复。有序代表存进集合是什么顺序,取出来就是什么顺序。有序还体现在List集合有下标。
  • Set接口:无序,不可重复。无序代表存进集合是什么顺序,取出来不一定是这个顺序。Set集合没有下标。

 065ac4560d78434484083c286d6eaf42.jpg

 Collection的contains()和remove()方法底层调用了equals()方法,所以会比对和操作与传入的参数“值”相同的元素。放入集合的对象都应该重写equals()方法。

86e49af3d8a44593a500608c9c783b68.jpg

 运行结果

0

remove()可以传对象或者索引,这是两个不同的remove

如果向集合中添加了两个值同为“hello”的对象,用remove("hello")删除时,只删除其中一个。

List集合

特有的方法add(索引,元素)

List list=new Arraylist();

list.add("a");

list.add(1,"king");

list.add("b");

//输出的代码省略

运行结果

a

king 

b

这种add()方法使用的不多,因为效率低。Arraylist底层是数组,因为内存是连续的,会涉及一系列元素的向前或向后移动。Collection的add()方法添加在末尾,不影响。

ArrayList
  1. 底层是object数组,默认初始化长度10,每次扩容到原来的1.5倍
  2. 不是线程安全的
  3. 因为数组的特性,查询效率高,随机增删效率低。但一般都在集合末尾添加元素,所以ArrayList是使用最多的。
  4. ArrayList有两种构造方法,一种是空参构造,另一种是传一个Collection数组
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

LinkedList 

以下是单向链表,LinkedList实现是双向链表

12526480a67340b58aebdd93eafea5fd.jpg

双向链表

f325adf50bb44e43b64cf1fd8113cd79.jpg

LinkedList源码

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    //指向链表中第一个节点
    transient Node<E> first;
    //指向链表中第二个节点
    transient Node<E> last;
}
//LinkedList部分代码
//内部类
 private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
//LinkedList部分代码
//add方法
  public boolean add(E e) {
        linkLast(e);
        return true;
    }
//LinkedList部分代码
 void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

e9dfbb097d834d848c7dda4df7c3a1c1.jpg

Vector

初始化容量默认10

每次扩容至原来的2倍

vector源码

public Vector() {
    //空参构造调有参构造
        
     this(10);
    }

public Vector(int initialCapacity) {
    //有参构造调另一个有参构造
    this(initialCapacity, 0);
    }

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    //初始化容量非法
    if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
    //创建容量为10的数组
    this.elementData = new Object[initialCapacity];
    //容量扩容0
    this.capacityIncrement = capacityIncrement;
    }
public synchronized boolean add(E e) {
    modCount++;
    //elementCount是实例变量,创建vector对象后自动赋值0
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
    }

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    //集合的size()是元素个数,数组的length是数组长度
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
    }
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")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
//调用底层c++
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

问题:如何把线程不安全的ArrayList变成线程安全的

java.util.collections工具类(不是collection接口)

List list=new ArrayList();
Collections.synchronizedList(list);

泛型

泛型是jdk5之后的新特性

jdk8之后引用了自动类型推断(又称钻石表达式)

List<String> list1=new ArrayList<String>(); 
List<String> list2=new ArrayList<>();//自动类型推断
 自定义泛型

<>里是标识符,可以随便写

源码中一般是E(Element)或者T(Type)

3aecf4278a57452bb3b20470bdff1bf7.png

ff436a2e40ec482da8530565f5bf2b2d.png

类定义了泛型,但创建对象时不使用,默认Object类型

Map集合

43cef046ab6d4d619460f9657309223d.jpg

常见方法

1.boolean containsKey(Object key)

2.boolean containsValue(Object value)

这两个方法底层调用的都是equals(),所以key和value使用的引用类型要重写equals()

3.Set<Object> keySet() 返回一个装有全部key的set集合

    Map<Integer,String> map=new HashMap();
        map.put(1,"zhangsan");
        map.put(2,"lisi");
        map.put(3,"wangwu");
        map.put(4,"zhaoliu");
        //先拿到key,根据key获取value
        Set<Integer> keys = map.keySet();
        //迭代器
        Iterator<Integer> it = keys.iterator();
        while (it.hasNext()){
            Integer next = it.next();
            System.out.println(map.get(next));
        }
        //增强for
        for(Integer key:keys){
            System.out.println(map.get(key));
        }

4.Set<Map.Entry<K, V>> entrySet() 返回一个set集合,元素类型是Map.Entry<K, V>

Map.Entry是一个接口

HashMap中有一个内部类Node实现了这个接口

所以这个方法其实返回的是元素为Node类型的set集合

//该类重写的toString()
public final String toString() { return key + "=" + value; }
    Set<Map.Entry<Integer, String>> set = map.entrySet();
        //迭代器
        Iterator<Map.Entry<Integer, String>> iterator = set.iterator();
        while (iterator.hasNext()){
            Map.Entry<Integer, String> Node = iterator.next();
            System.out.println(Node);
        }

        //增强for
        for(Map.Entry<Integer,String> node: set){
            //也可以使用node.getKey(),getValue()分别获取
            //这两个方法是Node从Map.Entry继承的
            System.out.println(node);
        }
HashMap

1.HashMap底层是哈希表/散列表的数据结构

2.哈希表是数组单向链表的结合

3.HashMap集合的默认初始化容量是16,默认加载因子是0.75

默认加载因子是当HashMap集合容量达到75%就开始扩容。

注意:HashMap初始化容量必须是2的次幂

4.jdk8后的新特性:如果哈希表单向链表中元素超过8,单向链表会转成红黑树数据结构;当红黑树上的节点小于6,会重新把红黑树变成单向链表数据结构。

5.HashMap扩容之后是原来的2倍。

6.HashMap的key和value都可以为空,但是只能有一个为null的key,重复添加value会覆盖。

7.通过get(null)可以找到对应的value

8.线程不安全

9.相同的key,hash值一定相同,因为hash值是hashcode()方法的执行结果,hash值可以通过hash算法转换成数组下标 tab[i = (n - 1) & hash] (n是数组长度)

HashSet源码

    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }
    //HashSet的add方法调用的是HashMap的put方法
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashMap源码

第一次put元素执行步骤

public class HashMap<K,V>{
    //HashMap底层是一个数组
    transient Node<K,V>[] table; 

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值,key的hashCode()方法的执行结果,通过哈希算法可以转换成下标
        final K key;
        V value;
        Node<K,V> next;
    }
}
    //HashMap的put方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
       int h;
        // ^ 异或运算 两个不同才为1
       return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
   }
    //HashMap中一个实例变量,new对象后默认为null
    transient Node<K,V>[] table;

    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)
            //第一个元素put,判断Node<K,V>[] table,是否为空,第一次空的,调用resize()扩容
            //扩容之后数组长度赋值给n,n=16
            n = (tab = resize()).length;
        }

    //实例变量,容量的阈值,超过这个值就要扩容,初始化为0
    int threshold;
    
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    tatic final float DEFAULT_LOAD_FACTOR = 0.75f;

    final Node<K,V>[] resize() {
        //把原来的数组复制一份
        Node<K,V>[] oldTab = table;
        //原来的数组为空,就给原容量赋值0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //阈值赋给原来的阈值
        int oldThr = threshold;
        //定义新容量(没赋值),新阈值赋值0
        int newCap, newThr = 0;
        //判断原容量是否>0,第一次put,oldCap=0,不走这个if
        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
        }
        //判断原阈值是否>0,第一次put这条分支也不走
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //第一次put直接进else
        else {               // zero initial threshold signifies using defaults
            //新容量赋值16,新阈值12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //新阈值不为0,这条不走
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //实例变量的阈值赋值成12
        threshold = newThr;
        //创建容量16的新数组
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        //第一次put不走
        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;
    }
//接着走没走完的putVal()方法
//p是一个Node,tab是一个Node数组,n=16,15&hash会得到一个0-15的int值
//算出来的值赋值给i,并访问tab数组下标为i的元素,赋值给p
//第一次数组所以下标位置都为null
    if ((p = tab[i = (n - 1) & hash]) == null)
            //在下标为i的位置创建节点
            tab[i] = newNode(hash, key, value, null);
    else{...} //此处不走else 代码省略

 类比

int i=0;
System.out.println(i=9);

运行结果 9

        int[] arr1=new int[10];
        //以下为局部变量,不会自动初始化,所以手动初始化0
        int p=0;
        int i=0;
        int n=0;
        arr1[3]=1;
        //if ((p = tab[i = (n - 1) & hash]) == null)
        // -1 & 3 =3 负数用补码&运算

        //1 0 0 0 0 0 0 1    -1原码
        //1 1 1 1 1 1 1 1    -1补码
        //0 0 0 0 0 0 1 1    3原码
        //0 0 0 0 0 0 1 1    -1 & 3
        int i1 = arr1[i = (n - 1) & 3];
        System.out.println(i1);

 运行结果 1

        Student[] students1=new Student[10];
        Student student=new Student();
        System.out.println((student=students1[0])==null);

运行结果 true

        ++modCount;
//添加第12个元素之后就要扩容了
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

不是第一次put,但算出的数组下标还没有元素时

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //虽然没有进入这个分支,但是tab和n的赋值在这里完成
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果该数组下标为空,可以直接添加新节点    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //........省略一段没执行的代码

        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

put(k,v)和get(k)实现原理

a9455bc9fcf04ea8aec70b85d8ee98ec.jpg

 注意:放在HashSet和HashMap的k部分的元素类型都要重写hashCode()和equals()。用IDEA一起重写。

Student类

//没有重写hashCode()和equals()
public class Student {
    private String name;

    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
    HashSet<Student> set=new HashSet();
        Student s1=new Student("zhangsan");
        Student s2=new Student("zhangsan");
        set.add(s1);
        set.add(s2);
        System.out.println(s1.hashCode()); //356573597
        System.out.println(s2.hashCode()); //1735600054
        System.out.println(set.size()); //2

HashSet的add()方法底层调用的是HashMap的put()方法,put()方法原理调用hashCode()得出来不同的hashCode,就会得出不同的下标。(不同的HashCode也有概率得到相同下标,称为哈希碰撞)该下标没有元素时,不会调用equals()方法,直接存进去。

这里因为没有重写两个方法,把相同的两个值存进HashSet了,这是不合理的。

重写之后的hashCode相同。

HashTable

1.HashTable的key和value都不能为空。

2.初始化容量11,扩容因子0.75

3.扩容为原来的2倍+1

Propreties

1.key和value只能是String

2.线程安全。

3.setPropreties()底层调用HashTable的put()方法,getPropreties()底层调用get()

TreeMap

1.无序,不可重复,可自动排序

2.放在TreeMap中的类必须实现java.lang.Comparable接口,重写CompareTo方法才能排序,不然会报错ClassCastException;或者创造比较器实现java.util.Comparator接口

3.如果比较规则只有一种,且不会轻易改变,建议实现Comparable接口。String和Integer都是实现的Comparable接口,因为数字和字符串的比较规则只有一种。如果比较规则有多个,并且需要来回切换,建议实现Comparator接口。这符合OCP原则(open closed principle开放封闭原则,对扩展开放,对修改关闭。体现在可以创建新的不同规则的比较器,而不是去修改原来的比较器)

4.TreeMap/TreeSet是一种平衡二叉树,遵循左小右大原则存放

5.遍历二叉树有三种方式:

        前序遍历 根左右

        中序遍历 左根右

        后序遍历 左右根

注意:前中后指的是根的位置,根在前面是前序,根在中间是中序...

6.TreeMap/TreeSet是中序遍历

    //TreeMap源码
    //空参构造 比较器默认空
    public TreeMap() {
        comparator = null;
    }

    //也可以调用有参构造传一个比较器
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    private final Comparator<? super K> comparator;//比较器
    //TReeMap的put方法源码
    public V put(K key, V value) {
        //声明唯一总根节点
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            //第一次传进来的作为根,第三个参数是parent根没有parent,所以为null
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        //声明根
        Entry<K,V> parent;
    
        Comparator<? super K> cpr = comparator;
        //如果采用空参构造创建的比较器为空,就走else分支
        //如果构造方法里传了比较器对象,走这个分支
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            //这里判断key是否为空是因为,下面要用key调用compareTo方法
            //上面一种是比较器调用compare方法,所以不用判断key是否空
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                //TreeSet中add的就是key
                //这里有强制类型转换
                //如果传进去的对象的类没有实现Comparable接口,就不能类型转换
                Comparable<? super K> k = (Comparable<? super K>) key;
            //采用do...while而不是while(){}原因:
            //第一次的t是根节点,如果走到这里根节点一定非空,不用判断,所以第一次直接do
            do {
                //t是父节点
                parent = t;
                //k是传进来的参数key,和根节点的key比较
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    //把当前父节点的左节点赋值给父节点,左节点变成新的根
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    //key相同就用新的value覆盖
                    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;
    }

实现比较器

1.创建一个类实现java.util.Comparator接口,重写compare()方法

public class Cat {
    int age;

    public Cat(int age){
        this.age=age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "age=" + age +
                '}';
    }
}

class CatComparator implements Comparator<Cat>{


    @Override
    public int compare(Cat o1, Cat o2) {
        return o1.age-o2.age;
    }
}
public static void main(String[] args) {

        TreeSet<Cat> treeSet=new TreeSet<>(new CatComparator());

        treeSet.add(new Cat(7));
        treeSet.add(new Cat(1));
        treeSet.add(new Cat(4));
        treeSet.add(new Cat(5));

        for(Cat c:treeSet){
            System.out.println(c);
        }
}

运行结果

Cat{age=1}
Cat{age=4}
Cat{age=5}
Cat{age=7}

2.不写实现类,采用匿名内部类

public static void main(String[] args) {

        TreeSet<Cat> treeSet=new TreeSet<>(new Comparator<Cat>(){
            @Override
            public int compare(Cat o1, Cat o2) {
                return o1.age-o2.age;
            }
        });

        treeSet.add(new Cat(7));
        treeSet.add(new Cat(1));
        treeSet.add(new Cat(4));
        treeSet.add(new Cat(5));

        for(Cat c:treeSet){
            System.out.println(c);
        }

    }

Integer和String的比较器已经写好升序排列,如果想降序排列可以重写比较器

public static void main(String[] args) {

        TreeSet<Integer> tree=new TreeSet<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                //TreeSet的add方法调用TreeMAp的put方法
                //put方法中传入compare()方法参数的顺序是compare(新元素,父节点)
                //按照Integer默认的compare方法,如果compare()返回的int大于0,
                //证明新元素比当前父节点大,要放在右子树
                //这里return o2-o1如果>0,t=t.right
                //代表父节点如果比新元素大,把新元素放在右子树,也就是降序排列,小的在后面
                return o2-o1;
            }
        });
        tree.add(1);
        tree.add(100);
        tree.add(30);
        tree.add(0);

        for(Integer i:tree){
            System.out.println(i);
        }


    }

Integer的compare()方法

public static int compare(int x, int y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

 String的compare()方法

public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

Collections工具类

Collections.sort()方法,参数传一个List集合,可以对List排序。前提是放到List集合中的对象所属类实现了Comparable接口。

public static void main(String[] args) {

        List<Student> list=new ArrayList<>();
        list.add(new Student("zhangsan"));
        list.add(new Student("lisi"));

        Collections.sort(list);
        for(Student s:list){
            System.out.println(s);
        }

    }

运行结果(按名字排序了,没排序前结果按添加顺序输出,即zhangsan lisi)

Student{name='lisi'}
Student{name='zhangsan'}

问题:因为sort()方法参数只能是List集合,如果想要给Set集合排序怎么办?

使用ArrayList的有参构造,把Set集合作为参数传进去,再用sort排序


流的分类

1.按照流的方向分,以内存为参照物

从硬盘到内存中去,叫做“输入”,或者叫做“读”;

从内存出去到硬盘。叫做“输出”,或者叫做“写”

2.按照读取方式分类

  1. 按字节读取,每次读取1个字节。这种流是万能的,可以读取任何类型的文件,如:文本文档、图片、视频、声音文件等

    假设file.txt中文件内容 a中国人bc,windows中英文1个字节,中文两个字节

            第一次读:一个字节,正好读到'a'

            第二次读:一个字节,正好读到“中”的一半

            第三次读:一个字节,正好读到“中”的另一办

  2. 按字符读取,一次读取一个字符。这种流只能读文本文档txt,连word都不能读

               第一次读:字符'a'

               第二次读:字符“中”

四大家族

InputStream 字节输入流

OutputStream 字节输出流

Reader 字符输入流

Writer 字符输出流

1.Java中的类,以Stream结尾的都是字节流,以ReaderWriter结尾都是字符流

2.以上四个都是抽象类

3.它们都实现了Closeable接口,接口中有close方法,也就是说它们都是可关闭的。

4.输出流都实现了Flushable接口,可刷新的,都有flush()方法,输入流都没有实现这个接口。

        输出流在最终输出之后,要记得flush()刷新。

        刷新表示将管道/通道中剩余未输出数据强行输出。

        刷新的作用就是清空管道。

        如果没有flush(),可能丢失数据。


FileInputStream

父类是InputStream

read()

这种read()方法一次只能读取一个字节,需要内存和硬盘频繁的交互,效率太低。

public class FileInputStreamTest01 {
    public static void main(String[] args) {
        FileInputStream fis=null;
        try {
            //相对路径
            //IDEA的当前路径默认是工程的根
            fis=new FileInputStream("src/com/whmc/io/tempfile");
            int read=0;
            //read()方法返回值是字符的ASII码值,空格也有ASII码,是32
            //读不到了返回-1,读不到和空格是两个概念
            while ((read=fis.read())!=-1){
                System.out.println(read);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

采用byte数组,一次可以读取数组.length个字节,返回值是读取到的字节数量,没有读到返回-1

    //tempfile内容:sfhvjjjnd
    fis=new FileInputStream("src/com/whmc/io/tempfile");
    byte[] bytes=new byte[4];
    int read=0;
    while ((read=fis.read(bytes))!=-1){
        System.out.println(read);
        System.out.println(new String(bytes));
            }

运行结果

4
sfhv
4
jjjn
1
djjn

但是我们希望字符串只保存读到的元素,所以可以用另一个构造方法

    while ((read=fis.read(bytes))!=-1){
        System.out.println(read);
        System.out.println(new String(bytes,0,read));
            }

运行结果

4
sfhv
4
jjjn
1
d

其他常用方法

1.int available() 返回流当中没有读到的剩余字节数量

    fis=new FileInputStream("src/com/whmc/io/tempfile");
    //在流刚创建的时候就获取剩余字节数,就可以把它作为数组长度,一次性读取文件内容
    //这种方法不适合大文件,因为数组内存是连续的
    int available=fis.available();
    byte[] bytes=new byte[available];

2.int skip(long n) 跳过几个字节不读

//文件内容:sfhvjjjnd
fis=new FileInputStream("src/com/whmc/io/tempfile");
fis.skip(3);
System.out.println(fis.read());

运行结果:118(v的ASII码)


FileOutputStream

父类是OutputStream

1.第一种write(),参数为int

public class FileOutPutStreamTest01 {
    public static void main(String[] args) {
        FileOutputStream fos=null;
        //文件不存在会自动创建
        try {
            fos=new FileOutputStream("src/com/whmc/io/testfile");
            fos.write(97);
            fos.write(98);
            fos.write(99);
            fos.write(100);
            //写完之后最后一定要刷新
            fos.flush();

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

2.第二种write(),参数为byte[]

fos=new FileOutputStream("src/com/whmc/io/testfile");
byte[] bytes={97,98,99,100};
fos.write(bytes);

3.第三种wrire(),参数为byte[],iny off,int len。把byte[]的一部分写出

fos=new FileOutputStream("src/com/whmc/io/testfile");
byte[] bytes={100,101,102};
fos.write(bytes,0,2);

这三种方式都是把原有文件的内容清空再写的

想要在文件末尾添加,采用FileOutputStream的另一个构造方法,参数里的boolean,true表示在末尾追加,false表示清空文件从开头覆盖。

fos=new FileOutputStream("src/com/whmc/io/testfile",true);

文件复制

public class Copy01 {
    public static void main(String[] args) {
        FileInputStream fis=null;
        FileOutputStream fos=null;
        try {
            fis=new FileInputStream("C:\\Users\\13155\\Downloads\\mate60.jpg");
            fos=new FileOutputStream("D:\\mate60.jpg");
            //一次读一个字节
            byte[] bytes=new byte[1024];
            int read=0;
            while ((read=fis.read(bytes))!=-1){
                //读到多少写多少字节
                fos.write(bytes,0,read);
            }
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //两个流不要放在一个try里关,因为如果有一个有异常,另一个可能关不了
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

FileReader

父类是InputStreamReader,InputStreamReader的父类是Reader

一次读取一个字符

public class FileReaderTest {
    public static void main(String[] args) {
        FileReader fr=null;
        try {
            fr=new FileReader("src/com/whmc/io/testfile");
            char[] chars=new char[4];
            int read=0;
            while ((read=fr.read(chars))!=-1){
                System.out.println(new String(chars,0,read));
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fr != null) {
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果

Chin
a中国

如果用FileInputStraem,长度为4的byte[]读运行结果为:

Chin

a中

FileWriter

父类是OutputStreamWriter,OutputStreamWriter的父类是Writer

ublic class FileWriterTest {
    public static void main(String[] args) {
        FileWriter fw=null;
        try {
            fw=new FileWriter("src/com/whmc/io/testfile");
            char[] chars={'J','A','V','A','软','件','开','发'};
            //可以从字符数组读
            fw.write(chars);
            //也可以从字符串读
            fw.write("\n");
            fw.write("工程师");
            fw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fw != null) {
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

普通文本复制

能用记事本打开、编辑的就是普通文本,和文件后缀无关。.java文件也是普通文本

public class Copy02 {
    public static void main(String[] args) {
        FileReader fr=null;
        FileWriter fw=null;
        try {
            fr=new FileReader("src/com/whmc/io/FileWriterTest.java");
            fw=new FileWriter("FileWriterTest.java");
            //一次读1kb,因为char是两个字节
            char[] chars=new char[512];
            int read=0;
            while ((read=fr.read(chars))!=-1){
                fw.write(chars,0,read);
            }
            fw.flush();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

BufferedReader

带有缓冲区的字符输入流

使用这个流的时候不需要自定义char数组或者byte数组,自带缓冲

public class BufferedReaderTest01 {
    public static void main(String[] args) throws Exception {
        FileReader fr=new FileReader("FileWriterTest.java");
        //当一个流的构造方法中需要一个流的时候,被传进来的流叫:节点流
        //外部负责包装的这个流叫:包装流,或者处理流
        //像当前程序,FileReader是节点流,BufferedReader是包装流
        BufferedReader br=new BufferedReader(fr);
        String s=null;
        //br.readLine()读取一个文本行,但不带换行符
        while((s=br.readLine())!=null){
            System.out.println(s);
        }
        //对于包装流来说,只用关闭最外层的流,里面的节点流会自动关闭,可以看源代码
        br.close();
    }
}

节点流自动关闭源代码

//BufferedReader构造方法,把节点流作为参数传入
public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }
//调用另一个构造方法,把节点流赋值给成员变量in
public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        //in是成员变量
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }
public class BufferedReader extends Reader {

    private Reader in;
}
//BufferedReader的close方法
public void close() throws IOException {
        synchronized (lock) {
            if (in == null)
                return;
            try {
                //在这里关闭的节点流
                in.close();
            } finally {
                in = null;
                cb = null;
            }
        }
    }

把字节流FileInputStream通过转换流InputStreamReader变成字符流,包装进BufferedReader

public class BufferedREaderTest02 {
    public static void main(String[] args) {
        BufferedReader br=null;
        try {
            //这是一个字节流,但是BufferedReader中只能传字符流
            FileInputStream fis=new FileInputStream("FileWriterTest.java");
            //InputStreamReader是一个转换流,可以把字节流转成字符流
            //参数需要一个InputStream,FileInputStream的父类是InputStream
            //在这里fis是一个节点流,reader是一个包装流
            InputStreamReader reader=new InputStreamReader(fis);
            //在这里reader是一个节点流,br是一个包装流
            br=new BufferedReader(reader);
            String s=null;
            while ((s=br.readLine())!=null){
                System.out.println(s);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
//合并写法
br=new BufferedReader(new InputStreamReader(new FileInputStream("FileWriterTest.java")));

BufferedWriter

带缓冲区的字符输出流

public class BufferedWriterTest01 {
    public static void main(String[] args) {
        BufferedWriter br=null;
        try {
            //br=new BufferedWriter(new FileWriter("Copy01"));
            br=new BufferedWriter(new OutputStreamWriter(new FileOutputStream("Copy01")));
            br.write("hello kitty!");
            br.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

BufferedInputStream

带缓冲区的字节输入流

public class BufferedInputStreamTest {
    public static void main(String[] args) {
        BufferedInputStream bis=null;
        try {
            bis=new BufferedInputStream(new FileInputStream("src/com/whmc/io/testfile"));
            int read=0;
            while ((read=bis.read())!=-1){
                System.out.println(read);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

BufferedOutputStream

带缓冲区的字节输出流

public class BufferedOutputStreamTest {
    public static void main(String[] args) {
        BufferedOutputStream bos=null;
        try {
            bos=new BufferedOutputStream(new FileOutputStream("Copy01",true));
            byte[] bytes={97,98,99};
            bos.write(bytes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

DataOutputStream

数据字节输出流,可以把数据以及它们的数据类型一起保存到文件,该文件不是普通文本文件,记事本打开是乱码,只能用DataInputStream获取数据。而且还需要知道写入规则,按照写的顺序读

public class DataOutputstreamTest {
    public static void main(String[] args) {
        DataOutputStream dos=null;
        try {
            dos=new DataOutputStream(new FileOutputStream("data"));
            byte b=97;
            short s=200;
            int i=300;
            long l=400l;
            float f=1.5f;
            double d=3.14;
            boolean sex=true;
            char c='w';
            dos.writeByte(b);
            dos.writeShort(s);
            dos.writeInt(i);
            dos.writeLong(l);
            dos.writeFloat(f);
            dos.writeDouble(d);
            dos.writeBoolean(sex);
            dos.writeChar(c);

            dos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                dos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

DataInputStream

数据字节输入流

public class DataInputStreamTest {
    public static void main(String[] args) {
        DataInputStream dis=null;
        try {
            dis=new DataInputStream(new FileInputStream("data"));
            byte b = dis.readByte();
            short s = dis.readShort();
            int i = dis.readInt();
            long l = dis.readLong();
            float f = dis.readFloat();
            double d = dis.readDouble();
            boolean sex = dis.readBoolean();
            char c = dis.readChar();
            System.out.println(b);
            System.out.println(s);
            System.out.println(i);
            System.out.println(l);
            System.out.println(f);
            System.out.println(d);
            System.out.println(sex);
            System.out.println(c);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (dis != null) {
                try {
                    dis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

PrintStream

标准字节输出流,可以改变文件输出的方向,输出到控制台或者文件

public class PrintStreamTest {
    public static void main(String[] args) {
        System.out.println("hello world");
        //System.out是一个static final修饰的PrintStream类变量
        PrintStream printStream=System.out;
        printStream.println("hellp kitty");

        //标准输出流不用手动close
        //标准输出流可以改变输出方向,不输出到控制台,输出到文件
        //之前用过的System类的方法和属性
//        System.gc();
//        System.currentTimeMillis();
//        System.arraycopy();
//        System.exit(0);
//        PrintStream printStream1 = System.out;

        //标准输出流不再指向控制台,指向log文件
        //PrintStream构造方法需要一个参数OutputStream
        try {
            PrintStream printStream1=new PrintStream(new FileOutputStream("log"));
            //改变输出方向,输出到log文件
            System.setOut(printStream1);
            System.out.println("helllo world");
            System.out.println("hello kitty");
            System.out.println("hello zhangsan");
            //重新输出到控制台
            System.setOut(printStream);
            System.out.println("mac");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

可以用于生成日志文件

public class Logger {
    public static void log(String msg){
        try {
            PrintStream out=new PrintStream(new FileOutputStream("logfile.txt",true));
            System.setOut(out);
            Date now=new Date();
            SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String s = simpleDateFormat.format(now);
            System.out.println(s+":"+msg);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
public class LogTest {
    public static void main(String[] args) {
        Logger.log("用户调用了System.gc(),建议启动垃圾回收器");
        Logger.log("用户调用了UserService.dosome()");
        Logger.log("用户尝试登录,验证失败");
    }
}
logfile.txt
2024-03-25 11:42:58 925:用户尝试登录,验证失败
2024-03-25 11:43:39 646:用户调用了System.gc(),建议启动垃圾回收器
2024-03-25 11:43:39 670:用户调用了UserService.dosome()
2024-03-25 11:43:39 670:用户尝试登录,验证失败


PrintWriter

标准字符输出流

public class PrintWriterTest {
    public static void main(String[] args) {
        try {
            PrintWriter pw=new PrintWriter(new FileWriter("logfile.txt",true));
            Date date=new Date();
            SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String now = simpleDateFormat.format(date);
            pw.print(now+":using PrintWriter");
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2024-03-25 12:00:00 609:using PrintWriter

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值