Java 基础巩固(一)

110 阅读7分钟

Java 基础巩固(一)

一、多态复习

多态案例

public static void main(String[] args) {
    Master master = new Master("tom");
    Dog dog = new Dog("小黄");
    Bone bone = new Bone("骨头");
    master.feed(dog, bone);
    Cat cat = new Cat("tom猫");
    Fish fish = new Fish("黄花鱼");

    master.feed(cat, fish);
    // 编译类型 看 = 左边,不能改变
    // 运行类型 看 = 右边,可以改变
}

class Master {
    private String name;
    public void feed(Animal animal, Food food){
        System.out.println("主人" + name + "给" + animal.getName() + "吃" + food.getName());
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Master(String name) {
        this.name = name;
    }
}
class Animal{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Animal(String name) {
        this.name = name;
    }
}
class Food{
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

class Fish extends Food{
    public Fish(String name) {
        super(name);
    }
}

class Bone extends Food {
    public Bone(String name) {
        super(name);
    }
}
class Cat extends Animal{
    public Cat(String name) {
        super(name);
    }
}

class Dog extends Animal{
    public Dog(String name) {
        super(name);
    }
}

1.1 多态需要注意的点

  • 多态的前提是:两个类存在继承关系
  • 多态的向上转型
    • 需要遵守访问规则
    • 可以调用父类所有成员
    • 不能调用子类特有方法
    • 能调用那些成员,是由编译类型所决定
// 向上转型 : 父类引用指向子类
// 语法:      父类引用名 父类名 = new 子类名();
Animal animal = new Cat();
  • 向下转型
    1. 语法:子类类型 引用名 = (子类类型) 父类引用;
    2. 只能强转父类引用,不能强转父类类型
    3. 要求父类引用必须指向当前目标类型对象(转型之前,需要父类指向子类)
    4. 当向下转型后可以调用子类类型中的所有成员
Animal animal = new Cat();
Cat cat = (Cat) animal;
cat.catchMouse();
  • 属性没有重写一说!属性的值看编译类型
  • instanceof 用来判断对象的运行 类型是否为 XX 类型或 XX 类型的子类型
Base base = new Sub();
System.out.println(base.count); // 10
Sub sub = new Sub();
System.out.println(sub.count); // 20

1.3 动态绑定机制

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

二、集合

  • 集合分为两种:

    1. 单列集合
    2. 双列集合
    3. Collection 接口有两个重要的子接口 :List 、Set
    4. Map 的子接口: HashMap、HashTable、LinkedHashMap
  • 增强 for 也就是简化版本迭代器,底层也是迭代器

2.1 List 接口

  • 可以重复
  • 取出的顺序和存入的顺序一样
  • List 子类
    • ArrayList
    • LinkedList
    • Vector

2.1.1 ArrayList 源码解析

  • ArrayList 线程不安全

  • ArrayList 中维护了一个elementData数组

    • 如果调用无参构造器,该数组大小默认大小为 0
    • 如果调用有参构造器,如果该数组满了,数组大小按照1.5倍递增
    • 如果使用指定大小构造器,那么elementData的大小为指定大小,如果需要扩容,也是按照1.5倍扩容
  • transient :使用该属性修饰对象,代表该对象不会被序列化

使用无参构造器,创建和使用 ArrayList 的源码

  1. 首先创建了一个空数组elementData image.png
  2. 执行 add 方法 image.png
    • 首先让结构性修改次数+1,modCount为全局变量,用来记录结构性修改次数 image.png

      image.png

      image.png

    • 然后在调用让elementData数组增加的add方法,如果数组长度满了,调用grow()方法对数组进行扩容

    • Arrays.copyOf()保留原来的数据,增加容量

分析有参构造器源码,指定大小,创建ArrayList

image.png

2.1.2 Vector 源码解析

  • 如果是无参,默认10容量,容量满了之后,按照2倍扩容
  • 如果指定大小,则每次直接按照2倍扩容
  • 和ArrayList源码步骤一样,只不过一个是线程安全,一个线程不安全

2.1.3 LinkedList 源码解析

  • 底层实现:双向链表 + 双端队列
  • 线程不安全,没有实现同步
  • LinkedList底层维护了一个双向链表
  • LinkedList中维护了两个属性:first和last,分别指向头结点和尾结点
  • 每个节点(Node对象),里面又维护了prev、next、item三个属性
  • LinkedList的增删通过链表完成,效率高

linkedList : add()源码分析

  1. 当调用add方法时,会调用linkLast方法
public boolean add(E e) {
    linkLast(e);
    return true;
}
  1. linkLast方法,首先会创建一个新的Node节点,将值放进节点,会判断当前有几个节点,如果只有一个,那么首位都是新建立的节点,如果有多个,那么就会将该节点链接到后面
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++;
}
  1. Node 节点的结构
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 的 remove()源码分析

  1. 首先会跳到 removeFirst()方法,因为底层是双向链表+双向队列的方式,从开端开始移除
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
  1. 当移除成功后,会调整剩余元素的指针链接情况
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
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

2.1.4 ArrayList 和 LinkedList 比较

image.png

  • 如果改查比较多,选择ArrayList
  • 如果增删比较多,选择LinkedList

2.2 Set 接口

  • 无序
  • 不允许重复元素

2.2.1 HashSet

  • 构造器源码的底层其实是HashMap
  • 底层构造:数组 + 链表 + 红黑树
public HashSet() {
    map = new HashMap<>();
}
  • 不能有重复元素/对象
  • HashSet添加元素源码分析(类似数据结构中的哈希冲突解决方法
    1. 添加元素时,先得到 hash 值 - 根据 hash mod key -> 索引值
    2. 找到哈希存储表table,看该索引的位置上是否存在元素
    3. 如果没有,直接放入
    4. 如果有,调用 equals 比较,如果相同,放弃添加,如果不同,添加到最后
    5. 在jdk8中,如果链表元素个数超过 8 ,并且table大小超过64,默认转成红黑树
    6. HashSet的扩容和转成红黑树机制
      1. HashSet底层是HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16*加载因子 (loadFactor)是0.75 = 12
      2. 如果table数组使用到了临界值12,就会扩容到162 = 32,新的临界值就是320.75 = 24,依次类推
      3. 在Java8中,如果—条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小>= MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制
    7. 具体实现步骤如下
      // 1. 执行 HashSet()
      //    构造器其实就是 Map
      // 2. 执行 add()
          public boolean add(E e) {
              return map.put(e, PRESENT)==null;   PRESENT = new Object()
          }
      // 3. 执行 put() 该方法执行 hash(key)方法,得到 key 对应 hash 值  算法:h = key.hashCode() ^ (h >>> 16) 防止冲突
          public V put(K key, V value) {   // value = PRESENT, key = "java"
              return putVal(hash(key), key, value, false, true);
          }
      
       // 4.
          final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
              Node<K,V>[] tab; Node<K,V> p; int n, i; // 辅助变量
              // table 存放Node节点的数组 -> Node<K,V>
              // if 语句表示如果当前 table 是 null,或者 大小 = 0
              // 就是第一次扩容,默认大小为 16
              if ((tab = table) == null || (n = tab.length) == 0)
                  n = (tab = resize()).length;
              // (1)根据 key , 得到 hash 值,去计算该 key 存到到 table 表那个索引位置
              //    并把这个位置的对象,赋给辅助变量 p
              // (2)判断 p 是否为 null
              // (2.1) 如果 p 为空,表示还没存放元素,就创建一个Node(key = "java", value=PRESENT)
              // (2.2) 就放在该位置 tab[i] = newNode();
              if ((p = tab[i = (n - 1) & hash]) == null)
                  tab[i] = newNode(hash, key, value, null);
              else {
                  // 开发技巧提示:在需要变量的地方,在创建
                  Node<K,V> e; K k;
                  // 如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样
                  // 并且满足下面两个条件之一:
                  //    (1) 对准备加入的 key 和 p 指向的 key 是同一个对象
                  //    (2) 或者 p 指向的 Node 结点 的 key 的 equals 方法 和 准备加入的 k 比较后相同
                  // 就不能加入
                  if (p.hash == hash &&
                      ((k = p.key) == key || (key != null && key.equals(k))))
                      e = p;
                  // 在判断这个 p 是不是一棵红黑树
                  // 如果是红黑树,就调用 putTreeVal 来进行添加
                  else if (p instanceof TreeNode)
                      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                  // 只有一种可能,是链表,接下来遍历链表,判断值是否相同,
                  //  (1)如果值不相同,就添加到链表最后,接着判断该链表是否达成 8 个结点
                  //        就调用 treeifyBin() 对当前链表进行树化
                  //        注意:在树化时:要进行判断,条件如下:
                          if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                              resize();  // 如果 table 数组的容量小于64
                  //        如果上面条件成立,就先扩容
                  //        只有上面条件不成立时,才进行树化
                  //   (2) 如果相同,就break
                  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;
          }
      

经典面试题

hashSet.add(new String("123")); // true
hashSet.add(new String("123")); // false
hashSet.add(new Dog("123")); // true
hashSet.add(new Dog("123")); // true

模拟简单的数组 + 链表结构

public static void main(String[] args) {
    Node[] nodes = new Node[5];

    nodes[0] = new Node(0);
    nodes[1] = new Node(1);
    nodes[2] = new Node(2);
    nodes[3] = new Node(3);
    nodes[4] = new Node(4);

    Node jack = new Node(5);
    nodes[0].next = jack;
    jack.next = new Node(6);

    for (Object o : nodes) {
        System.out.println(o);
    }


}
class Node{
    public Node next;
    public Object value;

    public Node(Object value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "next=" + next +
                ", value=" + value +
                '}';
    }
}

image.png

2.2.2 TreeSet

2.2.3 LinkedHashSet