面试题总结(自学笔记)

面试题总结

Java基础

1.面向对象的特征有哪些?

抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

继承:继承是从已知类得到继承信息创建新类的过程 提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段

封装:**通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。**我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。

多态:**多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。**多态性分为编译时的多态性和运行时的多态性。

2.Java的基本数据类型有哪些?

数值类型:byte(1字节),short(2字节),int(4字节),long(8字节)

浮点类型:float(4字节),double(8字节)

字符类型:char(2字节)

布尔类型:boolean(不同情况占用字节数不一样)

boolean只有两个值,true或false,可以用1bit来存储,大小没有明确规定,JVM会在编译时期将boolan类型转换为int类型,用1来表示true,0表示false,JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

3.String

概览

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)

在 Java 8 中,String 内部使用 char 数组存储数据。(名为value的数组)

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

不可变的好处(String为什么用final修饰?)

答:主要出于安全性效率的缘故。

1. 可以缓存 hash 值(效率)

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2. String Pool 的需要(效率)

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。String Pool节省了很多内存空间。

img

3. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下或者是String作为数据库连接的参数,如果 String 是可变的,那么在这些网络连接过程中,黑客们就可以通过改变字符串指向对象的值,造成安全漏洞。

4. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 和 s2.intern() 方法取得同一个字符串引用。intern() 首先把 “aaa” 放到 String Pool 中,然后返回这个字符串引用,因此 s3 和 s4 引用的是同一个字符串。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4);           // true

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6);  // true

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

3.Java语言的特点

  1. 简单易学,具有丰富的类库

  2. 面向对象(Java最重要的特性,提高了程序的内聚,降低了耦合)

  3. 跨平台(Java最大的特点。jvm是跨平台的基础和根本)

  4. 安全性高(主要体现在它的安全机制上)

    首先Java没有指针,一切内存的访问都需要通过类的实例来实现,因为Java严格遵循面向对象的规范,无指针运算,数组边界检查, 强制类型转换检查,Java的安全防范区域:byte-code验证器,类加载器,安全管理器。

  5. 支持多线程

4.面向对象和面向过程的区别

​ 面向对象:追求对现实世界的直接模拟,客观世界由具体的事物组成,而面向对象就是把这些事物封装成一个个对象,因为事物都有自己的属性和行为,所以对象也有,在解决问题的时候,就不是像面向过程一样在用的时候调用相应的函数,而是使用对象来描述某个事物在解决问题的过程中发生的行为。

​ 面向过程:可以理解为解决问题的步骤,然后用函数把这些步骤一步一步实现,在使用的时候调用即可。

5.Java的自动装箱和拆箱

装箱就是将基本数据类型自动转换成包装器类型(int–>Integer),调用integer的ValueOf(int)方法

拆箱就是将包装器类型自动转换成基本数据类型(Integer–>int),调用Integer的intValue方法

Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
int y = x;         // 拆箱 调用了 X.intValue()

在JavaSE5之前,生成一个数值为10的Integer类型的对象

Integer i = new Integer(10);

而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这
样就可以了

Integer i = 10;

5.1缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。反之如果值相同,但是范围不在-128-127之间呢?

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

  • boolean values true and false
  • all byte values
  • short values between -128 and 127
  • int values between -128 and 127
  • char in the range \u0000 to \u007F

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。

6.重写和重载的区别

重写:发生在父类和子类之间,子类继承父类,对父类中已有的方法进行重写,但是不改变参数个数和类型,返回值,只对方法体进行重写或修改。

重载:在一个类中,多个方法具有相同的方法名,但是有不同的参数列表,包括参数个数,类型顺序,称为方法重载。(对返回类型没有要求!!!)

7.equals与==的区别

**==:**比较的是变量(栈)内存中存放的对象的内存地址(堆)是否相等,即判断是否指向同一个对象。比较的真正意义是指针操作。

1.比较操作符两端是否指向同一个对象。

2.两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过

3.对于阿拉伯数字的比较,

例如 int a = 10;long a = 10L;double a = 10.0;指向的是同为10的堆

**equals:**用来比较两个对象的内容是否相等,由于所有的类都是继承自Java.lang.Object类,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然会是Object类中的方法,而Object类中的equals方法返回的却是==的判断。举例:

img

所有比较相等时都是用equals,并且对常量进行比较时,把常量卸写在前面,因为使用object的
equals object可能为null 则空指针

它的性质有:

  • 自反性(reflexive)。对于任意不为null的引用值x,x.equals(x)一定是true
  • 对称性(symmetric)。对于任意不为null的引用值xy,当且仅当x.equals(y)true时,y.equals(x)也是true
  • 传递性(transitive)。对于任意不为null的引用值xyz,如果x.equals(y)true,同时y.equals(z)true,那么x.equals(z)一定是true
  • 一致性(consistent)。对于任意不为null的引用值xy,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false
  • 对于任意不为null的引用值xx.equals(null)返回false

对于Object类来说,equals()方法在对象上实现的是差别可能性最大的等价关系,即,对于任意非null的引用值xy,当且仅当xy引用的是同一个对象,该方法才会返回true

需要注意的是当equals()方法被override时,hashCode()也要被override。按照一般hashCode()方法的实现来说,相等的对象,它们的hash code一定相等。

8.Hashcode的作用

Java中的集合Collection有两类,一类是List,一类是set,前者有序可重复,后者无序不重复。当我们在set中
插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法
就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对
象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就
可以确定该对象应该存储的那个区域。
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合
要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上
已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地
址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

9.集合详解

img
在这里插入图片描述

什么是集合?

对象的容器,实现了对对象常用的操作。

任何集合框架都包含三大块内容:对外的接口接口的实现(实现类)和对集合运算的算法

集合的特点:

  1. 对象封装数据,集合用于存储对象
  2. 对象的个数确定可以使用数组,不确定可以使用集合

集合和数组区别:

  1. 数组长度固定,集合长度不固定
  2. 数组可以存储基本数据类型和引用数据类;集合只能存储引用数据类型
  3. 数组存储的元素必须是同一数据类型,集合存储的对象可以是不同数据类型。

对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个容器的内部数据结构不同。**集合容器在向上抽取过程中,出现了集合体系,在使用一个体系的原则:参照顶层内容,建立底层对象。**每一种不同的集合实现类采用的底层实现方式都不一样,例如ArrayList底层数据结构是数组,TreeSet底层数据结构是红黑树。。。

迭代器(Iterator):专门用来遍历集合的一种方式

常用方法:

  • hasNext():判断是否有下一个元素,有则返回true,无则返回false
  • next():获取下一个元素
  • remove():删除元素

迭代器(Iterator)和列表迭代器(ListIterator)有什么区别?

  • Iterator可以遍历List和Set集合,但是ListIterator只能遍历List集合

  • Iterator只能单向遍历(向后),而ListIterator可以实现双向遍历(向前/向后遍历)

注意:当使用ListIterator向前遍历时,当前位置前面必须有元素,(当拿到一个List时,当前位置肯定在第一个位置,为了实现向前遍历,可以先向后遍历,然后再向前遍历)

  • ListIterator实现了Iterator接口,同时增加了一些额外的功能,比如增加一个元素,替换一个元素、获取前面或后面元素的索引位置。
ArrarList
  1. 数组结构实现,查询快,增删慢
  2. JDK1.2版本,运行效率快、线程不安全

ArrayList源码分析:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
      private int size;

默认容量:private static final int DEFAULT_CAPACITY = 10;

注意:如果没有向容器中添加任何元素,容量0,添加一个元素,容量10

当元素大小超过10时,每次扩容是原来的1.5倍!

存放元素的数组:transient Object[] elementData;

实际元素个数:private int size;

add():

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

  private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

多线程条件下如何使用ArrayList?

首先ArrayList不是线程安全的,不建议在多线程条件下使用ArrayList,如果非要使用,则可以使用Collections工具类的SynchronizedList方法将其转换成线程安全的类再使用。

LinkedList
  1. 链表结构实现,增删快,查询慢(双向链表)

在这里插入图片描述

LinkedList源码分析:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

    /**
     * Constructs an empty list.
     */

大小transient int size = 0;

头节点: transient Node<E> first;

最后一个节点:transient Node<E> last;

add():

public boolean add(E e) {
    linkLast(e);
    return true;
}
/**
 * Links e as last element.
 */
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++;
}
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;
    }
}
set接口
Hashset

特点:元素无序,唯一,基于HashMap实现,底层采用HashMap来保存元素。

存储结构分两步:

  1. 根据hashcode计算保存的位置,如果位置为空,则直接保存,如果不为空,则进行下一步
  2. 再根据equals方法进行判断,如果equals为true,则认为是重复,不添加,false则认为不重复,添加,形成链表
TreeSet

TreeSet的使用对于String类型和其他集合一样,因为TreeSet的存储结构是红黑树,红黑树是一种平衡二叉树。他需要给定一个判断依据判断添加的元素哪个更大,要将更大的数据放在根节点的右边,所以当添加的类型为自定义类型时,需要给出一个判断依据,具体有两种方法

  1. 在实体类中实现Comparable接口,并且重写compareTo方法
  2. 在new一个TreeSet对象的时候在new的参数里写一个compareTo

举例:

map接口
  • 特点:存储一对数据<键,值>。

    键:无序、无下标、不可重复(唯一),当键重复时,会把前面添加的键值对进行覆盖!

    值:无序、无下标、可以重复

  • 常用实现类:

    HashMap【重点】
    1. JDK1.2版本,线程不安全,运行效率快,允许null作为key和value
    2. 存储结构:哈希表(数组+链表+红黑树)

    HashMap源码分析:

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {
    
        private static final long serialVersionUID = 362498820763181265L;
        /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        /**
         * The maximum capacity, used if a higher value is implicitly specified
         * by either of the constructors with arguments.
         * MUST be a power of two <= 1<<30.
         */
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
        /**
         * The bin count threshold for using a tree rather than list for a
         * bin.  Bins are converted to trees when adding an element to a
         * bin with at least this many nodes. The value must be greater
         * than 2 and should be at least 8 to mesh with assumptions in
         * tree removal about conversion back to plain bins upon
         * shrinkage.
         */
        static final int TREEIFY_THRESHOLD = 8;
    
        /**
         * The bin count threshold for untreeifying a (split) bin during a
         * resize operation. Should be less than TREEIFY_THRESHOLD, and at
         * most 6 to mesh with shrinkage detection under removal.
         */
        static final int UNTREEIFY_THRESHOLD = 6;
    
        /**
         * The smallest table capacity for which bins may be treeified.
         * (Otherwise the table is resized if too many nodes in a bin.)
         * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
         * between resizing and treeification thresholds.
         */
        static final int MIN_TREEIFY_CAPACITY = 64;
    
        /**
         * Basic hash bin node, used for most entries.  (See below for
         * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
         */
        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
            transient Node<K,V>[] table;
        
    

默认初始容量大小:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 2的四次方=16

HashMap的数组最大容量:static final int MAXIMUM_CAPACITY = 1 << 30;

默认加载因子:static final float DEFAULT_LOAD_FACTOR = 0.75f;

  • 举例:当数组大小为100时,当数组里的数据大于75的时候,对数组进行扩容

当链表长度大于8时,调整为红黑树:static final int TREEIFY_THRESHOLD = 8;调整为红黑树是为了提高效率

当链表长度小于6时,调整为链表:static final int UNTREEIFY_THRESHOLD = 6;

当链表长度大于8时,并且集合元素个数大于64时,调整为红黑树:static final int MIN_TREEIFY_CAPACITY = 64;

哈希表中的数组:transient Node<K,V>[] table;

总结:

  1. HashMap刚创建时,table为null,为了节省空间,当添加第一个元素时,table容量调整为16
  2. 当table中元素大于阈值(16*0.75=12)时,会进行扩容,**大小为原来的2倍(**源码是原容量左移1位)。目的是减少调整元素个数
  3. JDK1.8以前,链表是头插入,JDK1.8以后是尾插入。

扩容机制以后详细讲解。。。。。。

HashTable

  • JDK1.0版本,线程安全,执行效率慢,不允许null作为key和value

Properties(HashTable的子类,用的比较多,通常跟流联系紧密,)

  • HashTable的子类,要求key和value都是String类型的,通常用于配置文件的读取。

10.泛型

  1. Java泛型是JDK1.5之后引入的一个新特性,其本质是参数化类型,把类型作为参数传递。
  2. 常用形式有泛型类,泛型接口,泛型方法
  3. 语法:<T,…>,T称之为类型占位符,表示一种引用类型,常用大写字母表示,如看K,V等
  4. 好处:
  • 提高代码重用性 (比如泛型方法)
  • 防止类型转换异常,提高代码的安全性

11.error和exception的区别

首先Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

Exception和Error体现了JAVA这门语言对于异常处理的两种方式。

Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。

Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。

其中的Exception又分为检查性异常非检查性异常。两个根本的区别在于,检查性异常 必须在编写代码时,使用try catch捕获(比如:IOException异常)。非检查性异常 在代码编写时,可以忽略捕获操作(比如:ArrayIndexOutOfBoundsException),这种异常是在代码编写或者使用过程中通过规范可以避免发生的。

12.JVM

内存模型

在这里插入图片描述

为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

  • 程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

(需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。)

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

  • 虚拟机栈和本地方法栈为什么是私有的?

  • 虚拟机栈每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程

  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

GC垃圾回收

在Java虚拟机中,本地方法栈、java虚拟机栈、程序计数器都是线程私有的,栈帧随着方法的进入和退出做入栈和出栈操作,实现了内存的自动清理,而堆和方法区是线程共享的,这部分区域的内存分配是动态的,因此GC垃圾回收主要发生在堆和方法区中

堆区域划分:

在这里插入图片描述

  • 伊甸园区(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。

  • 幸存者区(Survivor):从伊甸园幸存下来的对象会被暂时挪到这里。

  • 老年区(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进老年区时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。

GC(垃圾回收)类型:

  1. 新生代GC(Minor GC):对新生区进行垃圾回收
  2. 老年代GC(Major GC):对老年区进行垃圾回收( 对象进入老年代(大的直接 / 小的晋升)
    • 大对象:需要大量连续内存空间的Java对象。
    • 长期存活:多次Minor GC后仍然存活的对象。
  3. 全局GC(Full GC):Full GC 是针对整个新生代、老年代、元空间的全局范围的 GC。

GC(垃圾回收)流程:

在这里插入图片描述

  • 1、现在有一个新对象产生,那么对象一定需要内存空间,于是现在需要为该对象进行内存空间的申请。
  • 2、首先会判断伊甸园区是否有内存空间,如果此时有充足内存空间,则直接将新对象保存到伊甸园区。
  • 3、但是如果此时伊甸园区的内存空间不足,那么会自动执行Minor GC操作,将伊甸园区无用的内存空间进行清理。清理之后会继续判断伊甸园区空间是否充足?如果充足,则将新的对象直接在伊甸园区进行内存空间分配。
  • 4、如果执行Minor GC之后伊甸园区空间依然不足,那么这个时候会进行存活区判断,如果存活区有剩余空间,则将伊甸园区的部分活跃对象保存在存活区,随后继续判断伊甸园区的内存空间是否充足,如果充足,则进行内存空间分配。
  • 5、如果此时存活区也没有内存空间了,则继续判断老年区,如果此时老年区的空间充足,则将存活区中的活跃对象保存到老年区,而后存活区应付出现空余空间,随后伊甸园区将部分活跃对象保存地存活区中,最后在伊甸园区为新对象分配内存空间。
  • 6、如果这个时候老年代内存空间也满了,那么这个时候将产生Major GC(Full GC)。然后再将存活区中的活跃对象保存到老年区,从而腾出空间,然后再将伊甸园区的部分活跃对象保存到存活区,最后在伊甸园区为新对象分配内存空间。
  • 7、如果老年代执行Full GC之后依然空间依然不足,应付产生OOM(OutOfMemoryError)异常。

堆和方法区是所有线程共享的资源,其中:

  • 堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存)
  • 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

13.多线程

什么是进程和线程?

进程:在操作系统中能独立运行,并且作为资源分配的基本单位,它表示运行中的程序,系统运行一个程序就是线程从创建、运行到消亡的过程

线程:一个比进程更小的执行单位,能够完成一个进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程

什么是上下文切换?

即使是单核处理器也支持执行多线程代码,cpu通过给每个线程分配cpu时间片来实现这个机制。时间片是cpu分配给各个线程的时间,因为时间片非常短(时间片一般是十几毫秒),所以cpu通过不停的切换线程执行,让我们感觉多个线程是同时执行的。

CPU通过时间片分配算法来循环执行各个线程任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到恢复加载的过程就是一次上下文切换它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。上下文切换会影响多线程的执行速度

什么是并发和并行?

简单来说并发指的是多个任务交替进行,并行则是指真正意义上的多个任务“同时进行”

还有一种解释是并发是作用在同一个实体上的多个任务,而并行是作用在不同实体的多个事件

实际上,如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。

线程的生命周期和状态(重要)

每个线程各个时刻的状态无外乎这样五种:新生、就绪、运行、阻塞、死亡

  • 用new运算符建立了一个线程对象后,该对象就处于新生状态,处于新生状态的线程有自己的内存空间。

  • 当通过调用start()方法后,线程就进入了就绪状态,处于就绪状态的线程已经具备了运行的条件,但是还没有分配到CPU,因而将分配到线程队列,等待系统为其分配CPU,

  • 一旦获得CPU,线程就进入运行状态并自动调用自己的run()方法。

  • 处于运行状态的线程在某些情况下,如执行了sleep()方法,或等待I/O设备等资源,让出CPU并暂时终止自己的运行,此时线程进入**阻塞状态。**当阻塞状态解除后,线程进入就绪状态,等待CPU分配资源,当再次分配到资源时,便从原来终止位置开始继续运行。

其实网上也有一种说法是线程有六种状态:初始状态、运行状态、阻塞状态、等待状态、超时等待状态、终止状态,其实就是把各个状态细分了一下

  • 线程创建之后它将处于 初始状态(NEW),调用 start() 方法后开始运行,线程这时候处于 可运行状态(READY)。
  • 可运行状态的线程获得了 CPU 时间片后就处于 运行状态(RUNNING)。
  • 线程执行 wait()方法之后,线程进入 等待状态(WAITING),进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态【notify()】。 而 超时等待状态(TIME_WAITING)相当于在等待状态的基础上增加了超时限制,【sleep(long millis)/wait(long millis)】,当超时时间到达后 Java 线程将会返回到运行状态
  • 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到****阻塞状态(BLOCKED)。
  • 线程在执行 Runnable 的run()方法之后将会进入到 终止状态(TERMINATED)。

而线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换

在这里插入图片描述

什么是线程死锁?如何避免死锁?

死锁就是多个线程同时被阻塞,都在等待其中一个或多个线程的资源,这样线程就被无限期的阻塞 ,因此程序不能正常终止。程序反映出来的结果就是程序卡死,不动。

  • 产生死锁的四个必要条件:
  1. 互斥条件:一个资源每次只能被一个线程使用
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已持有的资源保持不放
  3. 不剥夺条件:进程以获得的资源,在未使用完之前,不能强行剥夺
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待关系

而避免死锁只需要破解以上一个或多个条件即可避免死锁

避免死锁的几个常见方法:

  • 设置加锁顺序

  • 设置加锁时限

  • 死锁检测

  • 避免一个线程同时获取多个锁

  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

  • 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。

  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

sleep()方法和wait()方法区别和共同点?(重点!)

**相同点:**两者都可以暂停线程的执行,使线程进入阻塞状态。

不同点:

  • sleep()方法没有释放锁,而wait()方法释放了锁。
  • sleep()方法属于Thread的静态方法,而wait()方法是Object类的实例方法,作用于对象本身。
  • 执行sleep()方法后,可以通过超时和interrupt()方法唤醒睡眠中的线程;执行wait()方法后,通过调用notify()或notifyAll()方法唤醒等待线程。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入初始状态;调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行

多线程开发带来的问题与解决办法?(重要)
  1. 线程安全问题

线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等

线程安全问题发生的条件:

1)多线程环境下,即存在包括自己在内存在有多个线程。

2)多线程环境下存在共享资源,且多线程操作该共享资源。

3)多个线程必须对该共享资源有非原子性操作。

线程安全问题的解决思路:

1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用

2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁

3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响

2.性能问题

线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。

解决思路:

利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。

3.活跃性问题

1)死锁假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。多个线程环形占用资源也是一样的会产生死锁问题。

解决方法:

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。

想要避免死锁,可以使用无锁函数(cas)或者使用重入锁(ReentrantLock),通过重入锁使线程中断或限时等待可以有效的规避死锁问题

2)饥饿,饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该线程无法执行等。

解决方法:

与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来尽量避免饥饿的产生

3)活锁,活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。

4.阻塞问题

阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。

解决方法:

可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能

临界区:

临界区是用来表示一种公共的资源(共享数据),它可以被多个线程使用,但是在每次只能有一个线程能够使用它,当临界区资源正在被一个线程使用时,其他的线程就只能等待当前线程执行完之后才能使用该临界区资源。

比如办公室办公室里有一支笔,它一次只能被一个人使用,假如它正在被甲使用时,其他想要使用这支笔的人只能等甲使用完这支笔之后才能允许另一个人去使用。这就是临界区的概念。

synchronized关键字

synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

synchronized关键字最主要的三种使用方式:修饰实例方法:、修饰静态方法、修饰代码块。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步代码块,锁是synchronized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

synchronized在JVM里是怎么实现的?

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)

synchronized用的锁是存在哪里的?

synchronized用到的锁是存在Java对象头里的。

synchronized和Lock的区别?(重要)

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断

4)synchronized是隐式的,Lock是显示的。 通过Lock可以知道有没有成功获取锁(tryLock()方法:如果获取锁成功,则返回true),而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

使用线程池的好处?
  1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗
  2. 提高响应速度当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
说一说几种常见的线程池及应用场景?(重要)

可以创建(Executors.newXXX)3种类型的ThreadPoolExecutor:FixedThreadPoolSingleThreadExecutorCachedThreadPool

  • FixedThreadPool:可重用固定线程数的线程池(适用于负载比较重的服务器)

    • FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列
    • 该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor:只会创建一个线程执行任务。(适用于需要保证顺序执行各个任务;并且在任意时间点,没有多线程活动的场景。)

    • SingleThreadExecutorl也使用无界队列LinkedBlockingQueue作为工作队列
    • 若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool:是一个会根据需要调整线程数量的线程池。(大小无界,适用于执行很多的短期异步任务的小程序,或负载较轻的服务器)

    • CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。
    • 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • ScheduledThreadPool:继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。使用DelayQueue作为任务队列。

线程池都有哪几种工作队列?(重要)
  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列
创建线程的几种方式?(重要)

有四种方式:继承Thread类、实现Runable接口、实现Callable接口、使用Executor框架来创建线程池

线程池参数

corePoolSize:线程池的基本大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。说白了就是,即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。

maximumPoolSize:最大线程数,不管你提交多少任务,线程池里最多工作线程数就是maximumPoolSize。

keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。

unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。

workQueue:用于保存等待执行任务的阻塞队列,提交的任务将会被放到这个队列里。

threadFactory:线程工厂,用来创建线程。主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。

handler:拒绝策略,即当线程和队列都已经满了的时候,应该采取什么样的策略来处理新提交的任务。默认策略是AbortPolicy(抛出异常),其他的策略还有:CallerRunsPolicy(只用调用者所在线程来运行任务)、DiscardOldestPolicy(丢弃队列里最近的一个任务,并执行当前任务)、DiscardPolicy(不处理,丢弃掉)

线程池执行流程

任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务。

14、IO流

Java中流的类型
  • 操作数据单位不同可以分为:字节流(8 bit),字符流(16 bit)

  • 流的流向不同可以分为:输入流,输出流

  • 流的角色不同可以分为:节点流,处理流

Java中流共设计到40多个类,其实非常具有规则性,都是从以下四个抽象基类派生而来

在这里插入图片描述

由这四个基类派生出来的子类名称都是以其父类名作为子类名后缀

Java IO里面的常见类,输入流、输出流、字节流,字符流、接口、实现类、方法阻塞

在这里插入图片描述

所谓输入和输出流是相对的,而我们只需要站在程序(内存)的角度来理解的话就很好理解

  • 输入流:读取外部数据(磁盘、光盘等存储设备)到程序(内存)中
  • 输出流:将程序(内存)中数据,输出到外部的磁盘、光盘等存储设备中。

对于字符流和字节流是指,传输流的过程采用的介质是字符(注意:这个字符只是接受字符,底层依然是字节)还是字节

  • 对于文本数据,适合采用字符流
  • 对于非文本数据(图片、视频…),采用字节流

java中的阻塞式方法是指在程序调用该方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如read()和readLine()方法。

字符流和字节流有什么区别?

字符流和字节流的使用非常相似,但是字节流操作实际上是对文件本身进行操作,不会经过缓冲区(内存),而字符流操作会先经过缓冲区(内存),然后再在缓冲区操作文件。

什么是Java序列化?如何实现序列化?

序列化就是一种用来将对象进行流化的机制,将对象进行流化,然后对流化后的对象进行读写操作,可以将流化后的对象传输于网络之间。序列化就是为了解决在对象读写操作时所引发的问题。

序列化的实现:将需要被序列化的类实现Serialize接口,没有需要实现的方法,此接口只是为了标注对象可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,再使用ObjectOutputStream对象的write(Object obj)方法就可以将参数obj的对象写出

PrintStream、BufferedWriter、PrintWriter的比较?
  • PrintStream类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出。它还提供其他两项功能。与其他输出流不同,PrintStream 永远不会抛出 IOException;而是,异常情况仅设置可通过 checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个 PrintStream
  • **BufferedWriter:将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符串的高效写入。**通过write()方法可以将获取到的字符输出,然后通过newLine()进行换行操作。BufferedWriter中的字符流必须通过调用flush方法才能将其刷出去。并且BufferedWriter只能对字符流进行操作。如果要对字节流操作,则使用BufferedInputStream
  • PrintWriter的println方法自动添加换行,不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生,PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush)
BufferedReader属于哪种流?它主要用来干什么?它里面有哪些经典的方法?

按照操作数据单位属于字符流,按照流向属于输入流,按照流的角色来分属于处理流,同时是处理流中的缓冲流,可以将读取的内容存到内存里面,经典的方法有readLine()方法,用来读取一行

什么是节点流,什么是处理流,它们各有什么用处,处理流的创建有什么特征?
  • 节点流:直接跟数据源进行相连,用来输入或者输出。
  • 处理流,在节点流的基础上对其进行加工,进行一些功能的扩展。
  • 处理流的构造器必须传入节点流的子类
流一般需要不需要关闭?如果关闭的话在用什么方法?一般要在那个代码块里面关闭比较好?处理流是怎么关闭的?如果有多个流互相调用传入是怎么关闭的?
  1. 流一旦打开就必须关闭,使用close方法
  2. 放入finally语句块中(finally 语句一定会执行)
  3. 调用的处理流就关闭处理流
  4. 多个流互相调用只关闭最外层的流
InputStream里的read()返回的是什么?read(byte[] data)是什么意思,返回的是什么值?
  1. 返回的是所读取的字节的int型(范围0-255)
  2. read(byte [ ] data)将读取的字节储存在这个数组。返回的就是传入数组参数个数
OutputStream里面的write()是什么意思,write(byte b[], int off, int len)这个方法里面的三个参数分别是什么意思?
  1. write将指定字节传入数据源
  2. Byte b[ ]是byte数组
  3. b[off]是传入的第一个字符、b[off+len-1]是传入的最后的一个字符 、len是实际长度

15、Spring

Spring是一款轻量级的开源的开发框架,简化了企业级应用开发,由于它是非侵入式的容器,并且是整合了很多框架技术,所以在很多地方都能用,spring的两个核心是IOCAOP

组成

在这里插入图片描述

img

spring框架是一个分层架构,由7个定义良好的模块组成, Spring模块构建在核心容器之上,核心容器定义了创建、配置、和管理bean的方式。

**核心容器(Core):**核心容器提供Spring框架的基本功能,负责管理组件的bean对象。核心容器的主要组件是BeanFactory,它是工厂模式的实现,BeanFactory使用控制反转(IOC)模式将应用程序开发的配置和依赖性规范与实际的应用程序代码分开。

Spring上下文:

Spring上下文是一个配置文件,向Spring框架提供上下文信息,Spring上下文包括企业服务,例如JNDI、EJB、国际化、电子邮件、校验和调度功能。

Spring AOP:

通过配置管理特性,Spring AOP模块直接将面向且面的编程功能集成到了Spring框架中,所以,可以很容易的使Spring框架管理的对象支持AOP,同时为基于Spring的应用程序的对象提供了事务管理服务,通过Spring AOP,不用依赖EBJ组件,就可以将声明式事务管理集成到应用程序中。

Spring DAO

JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。

Spring ORM:
Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。

Spring Web 模块:
Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。

Spring MVC 框架:

MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

Spring 框架的功能可以用在任何 J2EE 服务器中,大多数功能也适用于不受管理的环境。Spring 的核心要点是:支持不绑定到特定 J2EE 服务的可重用业务和数据访问对象。毫无疑问,这样的对象可以在不同 J2EE 环境 (Web 或 EJB)、独立应用程序、测试环境之间重用。

IOC:控制反转

IOC就是控制反转,是指创建对象的控制权发生了改变,以前创建对象的主动权和时机都是由程序员自己决定,而控制反转就是把这种权力转移给了spring容器,容器根据配置文件来创建实例和管理各个实例之间的依赖关系,这就使得了对象与对象之间松散了耦合性,提高了对功能和代码的复用性。

最直观的现象就是对象不用程序员自己new了,可以由spring自动产生,通过Java的反射机制,根据配置文件在程序运行时动态的去创建对象和管理各个对象之间的依赖。

SpringIOC有三种注入方式:

  1. 构造器注入
  2. setter方法注入
  3. 根据注解注入
AOP:面向切面编程

AOP一般称为面向切面编程,是作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取出来的一个模板,这个模板就背称为切面(Aspect),这样做减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于 代理模式AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理

​ ①JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。

​ ②如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

动态的去创建对象和管理各个对象之间的依赖。

SpringIOC有三种注入方式:

  1. 构造器注入
  2. setter方法注入
  3. 根据注解注入
AOP:面向切面编程

AOP一般称为面向切面编程,是作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取出来的一个模板,这个模板就背称为切面(Aspect),这样做减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于 代理模式AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理

​ ①JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。

​ ②如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理实例; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值