java(持续更新中)

java学习

java教程-廖雪峰

java教程-菜鸟

1. 面向过程

1. 自动类型转换

  1. 低精度的数据类型会进行自动类型转换成高精度类型

    byte b1 =10;//正确,尽管这是 int -> byte
    int n = 10;
    byte b2 = n;//错误,n 是一个 int 类型的变量,需进行强制类型转换
    

    当把具体的数赋值给 byte 时,先判断是否在 byte 范围之内(-128~127),如果在此范围即可转换

  2. 浮点数的默认为 double 类型

    int n= 10;
    float f = n+1.1;//错误
    float f = n+1.1F;//正确
    
  3. byte 与 short 类型不会和 char 类型之间相互自动转换

  4. byte,short,char 可以保存 int 的常量值(需要先判断范围是否符合),但是不能保存 int 变量值

  5. byte ,short 与 char 三者之间可以进行运算,计算时首先转换为 int 类型

    复合赋值运算符会自动进行强制类型转换,例如+=,-=,*=,/=,%=,++,--

    byte b1 = 1;
    byte b2 = 2;
    byte b3 = b1 + b2;//错误,运算时转换成 int 类型了
    byte b4 = 2;b4 +=2;//正确,复合赋值运算符会自动进行强制类型转换。例如+=,-=,*=,/=,%=,++,-- 
    short s = 1;
    char c = 1;//int 类型的常量值可以赋给 char
    int n = b1 + b2 + s + c;//正确
    
  6. 强制类型转换可能会造成精度损失或者数据溢出

2. 基本数据类型与String类型的相互转换

  1. 基本数据类型转String类

    将基本数据的值+“”即可

  2. String类转基本数据类型(要先确保能转换成有效数据)

    String s = "100";
    int  num = Integer.parseInt(s);
    

3. 原码,反码,补码

  1. 二进制的最高位是符号位,0表示正,1表示负(0->0,1->-)
  2. java没有无符号位
  3. 正数的原码,反码,补码都一样
  4. 负数的反码=原码符号位不变,其它位取反
  5. 负数的补码=反码+1(补码可以将正负数统一起来)
  6. 0的反码补码都是0
  7. 计算机运算时都是以补码方式来运算,看运算结果时要看原码

2. 面向对象

1.访问修饰符

访问级别 访问控制修饰符 同类 同包 子类 不同包
公开 public
受保护 protected ×
默认 × ×
私有 private × × ×

2. 继承(单继承机制)

  1. 子类继承的方法不能缩小访问权限;

  2. 子类继承了父类所有的属性和方法,非私有的属性和方法可以直接访问,但是私有的属性和方法不能在子类中直接访问,必须通过公共的方法去访问;

  3. 子类必须调用父类的构造器完成父类的初始化;

  4. 当创建子类的对象时,不管使用子类的哪个构造器,默认情况下总会调用父类的无参构造器super(),如果父类没有提供无参构造器,则必须在子类构造器中用super()去指定使用父类的哪个构造器完成对父类的初始化,否则编译不会通过;

  5. 如果子类的构造器希望指定调用父类的某个构造器,则显式调用super(参数列表);且必须放在构造器第一行,因为this(参数列表)也得放在第一行,所以这两个不能同时放在一个构造器中;

  6. super与this的比较表

    区别 this super
    访问属性 访问本类中的属性,如果本类没有则从父类中继续查找,本类有则不会继续在父类中查找 访问父类中的属性,父类没有就从父类的父类中查找,直到顶级父类
    调用方法 访问本类中的方法,如果本类没有则从父类中继续查找,本类有则不会继续在父类中查找 访问父类中的方法,父类没有就从父类的父类中查找,直到顶级父类
    调用构造器 调用本类构造器,必须放在构造器的第一行 调用父类构造器,必须放在子类构造器的第一行
    特殊 表示当前对象 子类中访问父类的对象

3. 多态

基础:
  1. 一个对象的编译类型和运行类型可以不一致(也可以一致)
  2. 编译类型在定义对象时就确定了,不能改变
  3. 运行类型是可以变化的
  4. 编译类型看 = 的左边,运行类型看 = 的右边
  5. 特点:可以调用父类中的所有可以访问的成员,不可以调用子类中特有的成员
难点:
  1. 动态绑定机制。当调用对象的方法时,该方法会和对象的内存地址/运行类型绑定当调用对象的属性时,没有动态绑定机制,哪里声明就哪里使用。
public static void main(String[] args) {
        A a = new B();
        System.out.println(a.sum());//40,若将B中的sum()去掉,则答案30
        System.out.println(a.sum1());//30,若将B中的sum1()去掉,则答案20
}
class A{
    public int i = 10;
    public int getI(){
        return i;
    }
    public int sum(){
        return getI() + 10;
    }
    public int sum1(){
        return i + 10;
    }
}
class B extends A{
    public int i = 20;
    public int getI(){
        return i;
    }
    public int sum(){
        return i + 20;
    }
    public int sum1(){
        return i + 10;
    }
}

4. 代码块(本质是对构造器的补充)

  1. static代码块也称为静态代码块,作用是对类初始化,随着类的加载而执行,且只会执行一次
  2. 普通代码块每创建一个对象就会执行(与类加载与否无关);
  3. 类什么时候被加载(也就是静态代码块什么时候执行)?
    1. 创建对象实例时(new)
    2. 创建子类对象实例,父类也会被加载
    3. 使用类的静态成员(静态属性或静态方法),此时普通代码块不会被执行
  4. 创建对象时一个类调用的顺序如下
    1. 调用静态代码块和静态属性初始化(优先级一样,所以若有多个静态代码块和多个静态属性初始化,按照定义顺序调用)
    2. 调用普通代码块和普通属性初始化(优先级一样,所以若有多个普通代码块和多个普通属性初始化,按照定义顺序调用)
    3. 调用构造方法
  5. 构造器的最前面隐含了super()和调用普通代码块、普通属性初始化
  6. 静态相关的代码块和属性初始化在类加载就执行(静态最先执行),优先于普通代码块、普通属性初始化和构造器
总结以上,创建一个子类对象时调用关系如下:
  1. 父类的静态代码块和静态属性初始化(优先级一样,按照定义的顺序执行);
  2. 子类的静态代码块和静态属性初始化(优先级一样,按照定义的顺序执行);
  3. 父类的普通代码块和普通属性初始化(优先级一样,按照定义的顺序执行);
  4. 父类的构造方法;
  5. 子类的普通代码块和普通属性初始化(优先级一样,按照定义的顺序执行);
  6. 子类的构造方法。
public static void main(String[] args) {
        B b = new B();
//        System.out.println(B.sA);//不会调用B的静态相关的
    }
class A {
    public int a = setA();
    public static int sA = setst();
    
    public A() {
        System.out.println("A Construct");
    }

    {
        System.out.println("A普通代码块执行");
    }

    static {
        System.out.println("A静态代码块执行");
    }

    public int setA() {
        System.out.println("A普通属性初始化");
        return 10;
    }

    public static int setsA() {
        System.out.println("A静态属性初始化");
        return 50;
    }
}

class B extends A {
    public int s = sets();

    {
        System.out.println("B普通代码块执行");
    }

    static {
        System.out.println("B静态代码块执行");
    }

    public B() {
        System.out.println("B Construct");
    }

    public int sets() {
        System.out.println("B普通属性初始化");
        return 10;
    }
}

5.单例设计模式

饿汉式VS懒汉式:

  1. 最主要的区别时创建对象时机不同:饿汉式在类加载就创建对象实例,懒汉式在使用时才创建;
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题;
  3. 饿汉式存在浪费资源问题,懒汉式不存在浪费资源问题;

6.final

  1. final修饰的属性是常量,一般用大写字母命名(XX_XX_XX);

  2. final修饰的属性在定义时,必须赋初值,且以后不可修改;

  3. final修饰的普通属性赋初值的位置如下:1.定义时;2.在构造器中,3.代码块中;

  4. final修饰的静态属性赋初值的位置如下:1.定义时;2.静态代码块中(不可以在构造器中);

  5. final类不能继承,但是可以实例化对象;

  6. 如果不是final类,但是还有final方法,则该方法虽然不可以被重写,但是可以被子类继承;

  7. 如果一个类是final类,则其方法没必要用final修饰;

  8. final不能修饰构造器;

  9. final和static搭配效率更高,因为不会导致类的加载

    public static void main(String[] args) {
          System.out.println(D.a);//只会输出100
        }
    class D{
        public static final int a=100;
        {
            System.out.println("普通代码块");//创建对象时执行;
        }
        static {
            System.out.println("静态代码块");//类加载时执行,未执行说明类未加载;
        }
    }
    

7.抽象类

  1. abstract只能修饰类和方法,不可用来修饰属性和其它的;
  2. 抽象类不能实例化对象;
  3. 抽象类可以没有abstract方法(抽象类可以有任意的成员),有抽象方法一定要声明为abstract类;
  4. 抽象方法不可以用private,final,static修饰,因为这些关键字与方法重写相违背;
  5. 抽象方法:访问修饰符 abstract 返回值类型 方法名(),没有方法体;

8.接口

  1. 接口不能实例化对象;
  2. 接口中的所有方法都是public(默认不是default)和abstract(可以省略不写);
  3. 一个类只能直接继承一个父类,但是可以实现多个接口;
  4. 接口中的属性只能是public final static修饰的(即int a = 10;等价public final static int a = 10;),因为是final,所以必须初始化属性
  5. 接口不能继承别的类,但是可以继承(extends)多个接口;
  6. 一个类可以实现(implements)多个接口;
  7. JDK1.8开始可以在接口定义普通方法必须default声明,调用方法:接口对象.函数名)和静态方法必须static声明,调用方法:接口名.函数名)。这些方法都会被实现接口的类继承。

9.接口与继承的对比

继承的目的:解决代码的复用性和可维护性,只能单继承,是is-a的关系;

接口的目的:设计好各种方法,让其他类去实现,更加灵活,可以实现多个接口,是like-a的关系;

10.内部类

内部类分类:

定义在外部类局部位置上(比如方法中):

​ 1.局部内部类(有类名)

​ 2.匿名内部类(无类名)

定义在外部类成员位置:

​ 1.成员内部类(无static修饰)

​ 2.静态内部类(有static修饰)

1. 局部内部类:
  1. 可以直接访问外部类的所有成员,包含私有的
  2. 外部类访问局部内部类成员需要先创建对象再访问,且需要在作用域内
  3. 不能添加访问修饰符修饰局部内部类,但是可以添加final修饰
  4. 作用域:仅仅在定义的方法或者代码块中
  5. 外部类和局部内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.this.成员名
2. 匿名内部类:(重要)
  1. 可以直接访问外部类的所有成员,包含私有的

  2. 匿名内部类既是一个,同时本身也为一个对象,所以可以作为参数

  3. 外部类和匿名内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.this.成员名

  4. 匿名内部类用来简化开发:当接口或者类的某个方法只需要使用一次时,无需创建一个类来implement接口了

    public static void main(String[] args) {
        new A(){
                @Override
                public void nice() {
                    System.out.println("简化开发");//外部类名$1
                }
            }.nice();
        A a = new A(){
                @Override
                public void nice() {
                    System.out.println("简化开发");//外部类名$2
                }
            };
        a.nice();
        System.out.println(a.getClass());
    }
    interface A {
        int n = 200;
    
    	void nice();
    }
    
  5. 匿名内部类的getClass()按照在外部类中的定义顺序依次为“外部类名$+序号" 序号=1,2,3……

  6. 语法:

new class or interface(填写参数){
    类体(方法重写)
};
3. 成员内部类:
  1. 可以直接访问外部类的所有成员,包含私有的

  2. 外部类访问成员内部类成员需要先创建对象再访问,且可以访问成员内部类的私有成员

  3. 外部其它类访问成员内部类 (前提是满足访问权限)

    Outer2 outer = new Outer2();

    1. Outer2.Inner InnerInstance1 = outer.new Inner();
    2. Outer2.Inner InnerInstance2 = outer.getInnerInstance();
  4. 可以添加任何修饰符修饰该类,因为它就是成员

  5. 作用域:作用于整个类体

  6. 外部类和成员内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.this.成员名

4. 静态内部类:(static修饰)
  1. 可以直接访问外部类的所有静态成员,包含私有的
  2. 外部类访问静态内部类成员需要先创建对象再访问,且可以访问成员内部类的私有成员
  3. 外部其它类访问静态内部类 (前提是满足访问权限),方法同成员内部类中
  4. 可以添加任何修饰符修饰该类,因为它就是成员
  5. 外部类和静态内部类成员重名时,遵循就近原则;若想访问外部类成员,可以用外部类名.成员名

11.String类(StringBuffer,StringBuilder)

![image-20220729205005460](D:\noteapp\java notebook\picture\image-20220729205005460.png)

  1. intern()返回的是常量池中的地址,new出来的String返回的是堆中的地址
  2. String两个常量相加看的是常量池;两个变量相加看的是堆,String常量+变量(变量+常量)看的也是堆,而且一定是new出来的
 		String a = "a";
        String b = "b";
        String sum1 = a + b;//创建了3个对象
        String sum2 = "a" + "b";//创建一个对象,编译器会优化,等价于String sum2 = "ab";
        System.out.println(sum1 == sum2);//false
    public static void main(String[] args) {
        //验证String的不可变
        String s = "hello";
        String02 string02 = new String02();
        string02.changeString(s);//非静态方法不可直接调用,需要通过对象调用
        System.out.println(s);

        StringBuffer sb = new StringBuffer("hello");
        changeStringBuffer(sb);
        System.out.println(sb);

    }

    public void changeString(String s) {
        s = s + "world";
    }

    public static void changeStringBuffer(StringBuffer sb) {
        sb.append("world");
    }
  1. String保存的字符串常量,里面的值不可修改(public final char value[]),每次String类更新实际是更改地址,效率低

  2. StringBuffer保存的字符串变量,里面的值可修改(char value[],存放在堆中),每次StringBuffer类更新实际是更改内容,不更新地址(只有空间不够才更新地址),效率高

  3. StringBuilder的方法没有互斥处理(没有synchronized关键字),因此在单线程情况下应用

  4. 三者比较

    String StringBuffer StringBuilder
    序列可变性 不可变字符序列 可变字符序列 可变字符序列
    效率 较高(增删) 最高
    优点 复用率高(常量池) 线程安全
    synchronized关键字
    线程不安全
    缺点 对字符串大量修改时
    不用String
  5. String 、 StringBuffer 和 StringBuilder 的选择使用的原则,结论:

    1. 如果字符串存在大量的修改操作,一般使用 StringBuffer 或 StringBuilder
    2. 如果字符串存在大量的修改操作,并在单线程的情况,使用 StringBuilder
    3. 如果字符串存在大量的修改操作,并在多线程的情况,使用 StringBuffer
    4. 如果我们字符串很少修改,被多个对象引用,使用 String ,比如配置信息等
    5. StringBuilder 的方法使用和 StringBuffer 一样,不再说
		long StartTime = 0L, EndTime = 0L;
        StringBuilder stringBuilder = new StringBuilder("");
        StartTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            stringBuilder.append(String.valueOf(i));
        }
        EndTime = System.currentTimeMillis();
        System.out.println("StringBuilder运行时间="+(EndTime - StartTime));

        StringBuffer stringBuffer = new StringBuffer("");
        StartTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            stringBuffer.append(String.valueOf(i));
        }
        EndTime = System.currentTimeMillis();
        System.out.println("StringBuffer运行时间="+(EndTime - StartTime));

        String s = "";
        StartTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            s += i;
        }
        EndTime = System.currentTimeMillis();
        System.out.println("String运行时间="+(EndTime - StartTime));

12.Collection

image-20220801175926837

List

1.ArrayList
  1. ArrayList 中维护了一个 Object 类型的数组 elementData(transient Object[] elementData; transient瞬间、短暂的,表示该属性不会被序列化)

  2. 当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为0,第1次添加,则扩容 elementData 为10,如需要再次扩容,则扩容 elementData 为1.5倍(即:size + size/2)

  3. 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为1.5倍

  4. 没有synchronized关键字,线程不安全

  5. ArrayList和Vector比较

    底层结构 线程和效率 扩容
    ArrayList transient Object[] elementData 不安全,效率高 有参时扩容每次1.5倍
    无参数时第一次扩容10,以后1.5倍
    Vector protected Object[] elementData 安全,效率不高 有参时扩容每次2倍
    无参数时第一次扩容10,以后2倍
2.LinkedList
  1. 线程不安全,增删效率高,改查效率低

Set:

  1. Set接口无序(添加和取出顺序不一致,每次取出的顺序固定),没有索引(所有不能使用普通for循环遍历),不允许重复元素(所以最多一个null)
1.HashSet(底层结构:数组+链表+红黑树)
添加元素机制:
  1. 先获取元素的哈希值hash( hashCode 方法,hash值≠hashCode)

  2. 对哈希值进行运算,得出一个索引值即为要存放在哈希表(transient Node<K,V>[] table;)中的位置号

  3. 如果该位置上没有其他元素,则直接存放;

    如果该位置上已经有其他元素,则需要进行 equals 判断:

    如果相等,则不再添加:

    Node<K,V> e; K k;
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
    

    如果不相等则以链表的方式添加:

    else {
        for (int binCount = 0; ; ++binCount) {//若table对应的索引位置是链表,依次在链表中比较
            if ((e = p.next) == null) {
                p.next = newNode(hash, key, value, null);
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);//添加完元素后判断该链表是否有8节点,达到则在treeifyBin中判断该链表是否要树化
                break;//添加元素成功
            }
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;//如果存在相同元素,则退出循环,添加失败
            p = e;//就是=p.next
        }
    }
    

    附:

    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;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            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);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            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)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    //
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    
扩容机制:
  1. HashSet 底层是 HashMap(transient Node<K,V>[] table;),初始化若调用默认构造器,第一次添加时,table数组扩容到16,临界值(threshold)是16×加载因子(loadFactor是0.75)=12;若调用带参数构造器,则数组容量为≥initialCapacity的且能整除4的最小整数(未add元素之前,无论初始化构造器是否有参数,table=null)
  2. 如果 table 数组使用大于临界值12(每加入一个节点,table数组size++,不论是加在第一个还是链表中,当size>12就会扩容),就会扩容到16*2=32,新的临界值就是32×0.75=24,依次类推
  3. Java8中,如果一条链表的元素个数到达 TREEIFY _ THRESHOLD(默认是8)并且 tabe 的大小>=MINTREEIFY _ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
2.LinkedHashSet(底层结构:数组+双向链表)
  1. 在 LinkedHastSet 中维护了一个 hash 表和双向链表(LinkedHashSet 有 head 和 tail)

  2. 每一个节点有 before 和 after 属性,这样可以形成双向链表

  3. 在添加一个元素时,先获取元素的哈希值( hashCode 方法),再求索引,确定该元素在 table 的位置,然后将添加的元素加入到双向链表(如果已经存在,不再添加,添加元素机制原则和 hashset 一样

     tail.next = newElement 
     newElement.pre = tail 
     tail = newEelment ;
    
  4. 这样的话,我们遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致

13.Map

image-20220804162249760

  1. Map 与 Collection 并列存在。用于保存具有映射关系的数据: Key - Value
  2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
  3. Map 中的 key 不允许重复,value 可以重复,原因和 HashSet 一样,前面分析过源码
  4. Map 的 key 可以为 nul , value 也可以为 null ,注意 key 为 null ,只能有一个, value 为 null ,可以多个。
  5. 常用 String 类作为 Map 的 key
  6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value,get(key)方法
HashMap:(基本与HashSet一致)

image-20220805213543832

Hashtable:
  1. 与 HashMap 不同,Hashtable的 key 和 value 都不能为 null
  2. 底层 table 为 Hashtable$Entry[](private transient Entry<?,?>[] table;),初始大小11,数组扩容每次(×2+1)
posted @ 2022-08-06 01:14  ACtoYou  阅读(82)  评论(0)    收藏  举报