java面试自用

基础篇

一、面向对象和面向过程的区别

1、面向对象的特点

​ 什么是对象,简单来说对象就是现实世界存在的任何事物都可以称之为对象,有着自己独特的个性。

img

​ 面向对象就是构成问题事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个问题的步骤中的行为。

2、面向过程的特点

​ 什么是过程,简单理解为步骤,就是解决问题的顺序步骤。

img

​ 面向过程不同于面向对象,面向过程分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个一次调用就可以了。

3、面向对象和面向过程的区别

​ 用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。所谓盖浇饭,北京叫盖饭,东北叫烩饭,广东叫碟头饭,就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜。我觉得这个比喻还是比较贴切的。

蛋炒饭制作的细节,我不太清楚,因为我没当过厨师,也不会做饭,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。

蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。

到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。

盖浇饭的好处就是”菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是”可维护性“比较好,”饭” 和”菜”的耦合度比较低。蛋炒饭将”蛋”“饭”搅和在一起,想换”蛋”“饭”中任何一种都很困难,耦合度很高,以至于”可维护性”比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。

4、总结

​ 面向过程:

​ 优点:性能比面向对象高,因为类的调用需要实例化,开销比较大,比较消耗资源;如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能就是最重要的因素。

​ 缺点:没有面向对象易维护、易复用、易扩展

​ 面向对象:

​ 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护

​ 缺点:性能比面向过程低

二、基本类型和它的封装类

1、int与它的封装类

​ int是基本数据类型,integer是其封装类,是引用类型。

​ int的默认值是0,integer的默认值是null,所以integer可以区分0和null。

​ 在java中出现null,就知道这个引用还没有指向某个对象,在任何引用使用前,必须为其指定一个对象,否则会报错。

2、空间问题

​ 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须 通过实例化开辟数据空间之后才可以赋值。

​ 数组对象也是一个引用对象,将一个数组赋值给另一个数组 时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。

3、Boolean类型

​ 虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何 供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机 中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素 占8位。

​ 这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原 因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的是32/64位系统,而 是指CPU硬件层面),具有高效存取的特点。

三、命名规则

1、标识符的含义

​ 是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。

2、硬性命名规则

​ 可以包含英文字母,0-9的数字,$以及_

​ 不能以数字开头

​ 不是关键字

3、非硬性命名规则

​ 类名规范:大驼峰。

​ 变量名规范:小驼峰。

​ 方法名规范:小驼峰。

四、自动装箱、拆箱

​ 装箱:int–>Integer;调用方法:Integer的**valueOf(int)**方法

​ 拆箱:Integer–>int;调用方法:Integer的intValue方法

Integer a = 1;
Integer b = 1;
Integer c = 500;
Integer d = 500;
System.out.println(a == b);
System.out.println(c == d);
Integer aa=new Integer(10);
Integer bb=new Integer(10);
int cc=10;
System.out.println(aa == bb);
System.out.println(aa == cc);

答案是
true
false
false
true

​ Integer a = 1;是自动装箱会调用Integer.valueOf(int)方法;该方法注释如下:

​ This method will always *** values in the range -128 to 127 inclusive, and may *** other values outside of this range.

​ 也就是说IntegerCache类缓存了-128到127的Integer实例,在这个区间内调用valueOf不会创建新的实例。

​ Integer类型在-128–>127范围之间是被缓存了的,也就是每个对象的内存地址是相同的,赋值就直接从缓存中取,不会有新的对象产生,而大于这个范围,将会重新创建一个Integer对象,也就是new一个对象出来,当然地址就不同了,也就**!=**;

1、包装类和基本数据类型在进行“==”比较时,包装类会自动拆箱成基本数据类型,integer(0)会自动拆箱,结果为true
2、两个integer在进行“==”比较时,如果值在-128和127之间,结果为true,否则为false
3、两个包装类在进行“equals”比较时,首先会用equals方法判断其类型,如果类型相同,再继续比较值,如果值也相同,则结果为true
4、基本数据类型如果调用“equals”方法,但是其参数是基本类型,此时此刻,会自动装箱为包装类型

五、重载与重写

重写

​ 1.发生在父类与子类之间

​ 2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同

​ 3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)

​ 4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常

重载

​ 1.重载Overload是一个类中多态性的一种表现

​ 2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)

​ 3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准

六、equals和==

==

​ 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是 指相同一个对象。比较的是真正意义上的指针操作。

​ 1、比较的是操作符两端的操作数是否是同一个对象。

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

​ 3、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如: int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。

equals

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

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
总结

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

​ 在阿里的代码规范中只使用equals ,阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来排 查老代码使用“==”,替换成equals

七、instanceof

​ instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例

boolean result = obj instanceof Class

​ 其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或 间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。

使用范围:instanceof比较的是对象,不能比较基本类型,否则会报错;

​ 编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定 类型,则通过编译,具体看运行时定。

原理:class类型是否在obj所有父类和自身组成的集合M里(包括实现的接口),如果在result为true,如果不在为false;

int i = 0;
System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型
System.out.println(i instanceof Object);//编译不通过

Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true
System.out.println(null instanceof Object);//false ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回false。

八、集合

​ 集合用来存储不同类型的对象(基本数据类型除外),存储长度可变。
​ 集合中实际存放的只是对象的引用,每个集合元素都是一个引用变量,实际内容都放在堆内存或者方法区里面,但是基本数据类型是在栈内存上分配空间的,栈上的数据随时就会被收回的。

集合框架图

img

注:(两处错误)

​ ① List没有实现ListIterator接口,两者是关联关系

​ ② Hashtable继承的是Dictionary

​ 实线边框的是实现类,比如ArrayList,LinkedList,HashMap等,折线边框的是抽象类,比如AbstractCollection,AbstractList,AbstractMap等,而点线边框的是接口,比如Collection,Iterator,List等。

img

1、Iterator接口

​ (迭代器接口)用于遍历集合中元素的接口,主要包含三种方法:

boolean hasNext()
E next()
void remove()

​ 它的一个子接口ListIterator在它的基础上又添加了三种方法:

void add()
E previous()
boolean hasPrevious()

​ 实现Iterator接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历过的元素不会再遍历到,通常无序集合实现的都是这个接口,比如HashSet,HashMap;
而实现了ListIterator接口的集合,可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如List。

Iterator和Iterable的区别:
1). Iterator是迭代器接口,而Iterable是为了只要实现该接口就可以使用foreach进行迭代。

​ 2). Iterable中封装了Iterator接口,只要实现了Iterable接口的类,就可以使用Iterator迭代器了。

​ 3). 集合Collection、List、Set都是Iterable的实现类,所以他们及其他们的子类都可以使用foreach进行迭代。

​ 4). Iterator中核心的方法next()、hasnext()、remove()都是依赖当前位置,如果这些集合直接实现Iterator,则必须包括当前迭代位置的指针。当集合在方法间进行传递的时候,由于当前位置不可知,所以next()之后的值,也不可知。而实现Iterable则不然,每次调用都返回一个从头开始的迭代器,各个迭代器之间互不影响。

2、Collection

​ Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些Collection允许相同的元素而另一些不行,一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”,如List和Set。

​ 所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection;有一个Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。

如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子
while(it.hasNext()) {undefined
    Object obj = it.next(); // 得到下一个元素
}

(1)List:有序,可以存放重复的内容

(2)Set:无序,不能存放重复的内容,所有的重复内容靠hashCode()和equals()两个方法区分

(3)Queue:队列接口

(4)SortedSet:可以对集合中的数据进行排序

Collection定义了集合框架的共性功能。

img

① List

​ List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。

​ 和下面要提到的Set不同,List允许有相同的元素。

​ 除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。

​ 实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。

1.可以允许重复的对象。

2.可以插入多个null元素。

3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。

4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。

​ 其定义的方法包括:

int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);

List的子类的特点:

——ArrayList:

​ ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。

​ 每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。

​ 和LinkedList一样,ArrayList也是非同步的(unsynchronized)。

线程不安全,查询速度快。底层都是基于数组来储存集合元素,封装了一个动态的Object[]数组,是一种顺序存储的线性表。

——LinkedList:

​ LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。

注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:    List list = Collections.synchronizedList(new LinkedList(…));

线程不安全,增删速度快,没有同步方法,是一个链式存储的线性变,本质上是一个双向链表。

——Vector:

​ Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

线程安全,但速度慢,已被ArrayList替代。

——Stack:

​ Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

(已经不用,可由LinkedList代替)。

② set

​ Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。它的常用具体实现有HashSet和TreeSet类。

​ 很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。

​ 请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。

1.不允许重复对象

2.无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。

3.只允许一个 null 元素

4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。而且可以重复

其定义的方法包括:

int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();

set的子类的特点:

——HashSet:

​ HashSet能快速定位一个元素,但是你放到HashSet中的对象需要实现hashCode()方法,它使用了前面说过的哈希码的算法。使用该方式时需要重写 equals()和 hashCode()方法

底层数据结构由HashMap的键来实现。不保证集合中元素的顺序,即不能保证迭代的顺序与插入的顺序一致。是线程不安全的。

——TreeSet:

​ TreeSet则将放入其中的元素按序存放,这就要求你放入其中的对象是可排序的,这就用到了集合框架提供的另外两个实用接口Comparable和Comparator。一个类是可排序的,它就应该实现Comparable接口。有时多个类具有相同的排序算法,那就不需要再分别重复定义相同的排序算法,只要实现Comparator接口即可。

有序的存放,线程不安全,可以对Set集合中的元素进行排序,由红黑树来实现排序,TreeSet实际上也是SortedSet接口的子类,其在方法中实现了SortedSet的所有方法,并使用comparator()方法进行排序。

——LinkedHashSet:

​ 继承于 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMap

底层由链表实现,按照元素插入的顺序进行迭代,即迭代输出的顺序与插入的顺序保持一致

3、Map

​ Map是一种把键对象和值对象进行关联的容器,而一个值对象又可以是一个Map,依次类推,这样就可形成一个多级映射。对于键对象来说,像Set一样,一个Map容器中的键对象不允许重复,这是为了保持查找结果的一致性;如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求。你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。

​ 请注意,Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

​ 注意:由于Map中作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。

1.Map不是collection的子接口或者实现类。Map是一个接口。

2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。

3.TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。

4.Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。

5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)

实现Map接口的子类:

——Hashtable:

Hashtable继承Dictionary<K,V>类,实现了Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。是同步的。

​ 添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。

​ Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。

​ 使用Hashtable的简单示例如下,将1,2,3放到Hashtable中,他们的key分别是”one”,”two”,”three”:

Hashtable numbers = new Hashtable();
numbers.put(“one”, new Integer(1));
numbers.put(“two”, new Integer(2));
numbers.put(“three”, new Integer(3));

​ 要取出一个数,比如2,用相应的key:

Integer n = (Integer)numbers.get(“two”);
System.out.println(“two =+ n);

​ 由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。

​ 如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。

——HashMap:

HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。
但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

——LinkedHashMap:

是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现。Key和Value都允许空;Key重复会覆盖、Value允许重复;非线程安全;有序。

——TreeMap:

底层是二叉树数据结构。线程不同步。可以用于给map集合中的键进行排序。

九、八中的关系对比

1、HashMapHashTable

​ HashMap 是线程不安全的,HashMap 是一个接口,是 Map的一个子接口,是将键映射到值得对象,不允许键值重复,允许空键和空值;由于非线程安全, HashMap的效率要较 HashTable 的效率高一些.

​ HashTable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;

​ HashTable 是 sychronize(同步化),多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步;

1、两者父类不同

​ HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现 了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。

2、对外提供的接口不同

​ Hashtable比HashMap多提供了elments() 和contains() 两个方法。

​ elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的 value的枚举。

​ contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上, contansValue() 就只是调用了一下contains() 方法。

3、对null的支持不同

​ Hashtable:key和value都不能为null。

​ HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个key 值对应的value为null。

4、安全性不同

​ HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己 处理多线程的安全问题。

​ Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。

​ 虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的 使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。

​ ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为 ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。

5、初始容量大小和每次扩充容量大小不同

6、计算hash值的方法不同

2、Collections和Collection

​ Collection是集合框架中的一个顶层接口,它里面定义了单列集合的共性方法。
​ 它有两个常用的子接口:
​ ——List:对元素都有定义索引。有序的。可以重复元素。
​ ——Set:不可以重复元素。无序。

​ Collections是集合框架中的一个工具类。该类中的方法都是静态的。提供的方法中有可以对list集合进行排序,二分查找等方法。通常常用的集合都是线程不安全的。因为要提高效率。如果多线程操作这些集合时,可以通过该工具类中的同步方法,将线程不安全的集合,转换成安全的。

3、Array 和Arrays

​ array效率高,但容量固定且无法动态改变。
​ array还有一个缺点是,无法判断其中实际存有多少元素,length只是告诉我们array的容量。

​ Java中有一个Arrays类,专门用来操作array。
​ arrays中拥有一组static函数,

equals()//比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
fill()//将值填入array中。
sort()//用来对array进行排序。
binarySearch()//在排好序的array中寻找元素。
System.arraycopy()//array的复制。
4、Collection 和 Map
容器内每个为之所存储的元素个数不同。
 Collection类型者,每个位置只有一个元素。
 Map类型者,持有 key-value pair,像个小型数据库。
5、容器类和Array

​ 容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。
​ 一旦将对象置入容器内,便损失了该对象的型别信息。

6、使用的选择

​ 在各种Lists中,最好的做法是以ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList();
​ Vector总是比ArrayList慢,所以要尽量避免使用。
​ 在各种Sets中,HashSet通常优于HashTree(插入、查找)。只有当需要产生一个经过排序的序列,才用TreeSet。
​ HashTree存在的唯一理由:能够维护其内元素的排序状态。
​ 在各种Maps中:HashMap用于快速查找;当元素个数固定,用Array,因为Array效率是最高的。

​ **结论:**最常用的是ArrayList,HashSet,HashMap,Array。

7、hashcode

​ java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中 插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法 就会比较满。

​ 于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对 象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就 可以确定该对象应该存储的那个区域。

​ hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合 要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上 已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地 址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

8、ArrayList和linkedList

​ Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。

​ Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据, (因为删除数据以后, 需要把后面所有的数据前移)

​ 缺点: 数组初始化必须指定初始化的长度, 否则报错

int[] a = new int[4];//推介使用int[] 这种方式初始化
int c[] = {23,43,56,78};//长度:4,索引范围:[0,3]

​ List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection。

​ List有两个重要的实现类:ArrayList和LinkedList

​ ArrayList: 可以看作是能够自动增长容量的数组

​ ArrayList的toArray方法返回一个数组

​ ArrayList的asList方法返回一个列表

​ ArrayList底层的实现是Array, 数组扩容实现

​ LinkList是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于 ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。

十、String、String StringBuffer 和 StringBuilder

​ String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符 数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的 String对象。

private final char value[];

​ 每次+操作 : 隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法 拼接 +后面的字符。

​ StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder 抽象类中我们可以看到

char[] value;

​ 他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和 StringBuilder来进行操作。 另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是 线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

三者的继承结构

img

十一、创建对象的方式

1、new创建新对象
2、通过反射机制

①创建对象

​ 通过反射来生成对象有如下两种方式:

​ (1)使用Class对象的newInstance()方法来创建该Class对象对应类的实例。但是这种方式要求该Class对象的对应类有默认的构造器,而执行newInstance()方法时实际上是利用默认构造器来创建该类的实例对象。

​ (2)先使用Class对象获取指定的Constructor对象,再调用Construtor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用某个类的指定构造器来创建实例对象。

​ 另外,如果我们不想利用默认构造器来创建java对象,而想利用指定的构造器来创建java对象,则需要利用Construtor对象,每个Construtor对应一个构造器,为了利用指定构造器来创建java对象,需要如下三个步骤:

​ (1)获取该Class对象;

​ (2)利用该Class对象的getConstrutor方法来获取指定的构造器;

​ (3)调用Construtor的newInstance方法来创建Java对象。

②调用方法

​ 获取到某个类的Class对象之后,可以通过该Class对象的getMethods方法或者getMethod方法获取全部或指定方法。

每个Method对象对应一个方法,获得Method对象后,程序就可通过该Method来调用对应的方法,在Method里包含一个invoke方法,该方法签名如下:

​ Object invoke(Object obj,Object… args);该方法中的obj是执行该方法的主调,后面的args是执行该方法时传入该方法的实参。

当通过Method的invoke方法来调用对应的方法时,Java会要求程序必要要有调用该方法的权限。如果程序确实需要调用该对象的私有方法,则可先调用Method对象的:setAccessible(boolean flag);方法,将Method对象的accessoble标志设置为指示的布尔值。

布尔值为true,则表示该Method在使用时应该取消Java语言访问权限检查;

布尔值为false,则表示该Method在使用时应该实施Java语言访问权限检查;

③访问属性值

​ 通过Class对象的getFields或getField方法可以获取该类所包括的全部Field(属性)或指定Field,Field提供了如下两组方法来访问属性:

​ getXxx(Object obj);获取obj对象该Field的属性值,此处的Xxx对应8个基本类型,如果该属性的类型是引用类型则取消get后面的Xxx。

​ setXxx(Object obj,Xxx val);将obj对象的该Field设置成val值,此处的Xxx对应8个基本类型,如果该属性的类型是引用类型则取消set后面的Xxx。

④代码示例

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ClassTest {
    public static void main(String[] args) throws Exception {
        Object object;
        Class cl = Class.forName("TestMe");
        Method method = cl
                .getDeclaredMethod("print", new Class[]{String.class});
        Constructor constructor = cl
                .getDeclaredConstructor(new Class[]{String.class});
        object = constructor.newInstance(new Object[]{"Hello"});
        method.invoke(object, new Object[]{"zhouxianli"});
    }
}
class TestMe {
    private String str;
    public TestMe(String str) {
        this.str = str;
        System.out.println("In Constructor str = " + str);
    }
    public void print(String name) {
        System.out.println("In print str = " + str + " and name = " + name);
    }
} 
#### 3、采用clone机制 

什么是"clone"?

​ 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在 Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。

Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

①Clone&Copy

​ 假设现在有一个Employee对象,Employee tobby =new Employee(“CMTobby”,5000),通常我们会有这样的赋值Employee cindyelf=tobby,这个时候只是简单了copy了一下reference,cindyelf和tobby都指向内存中同一个object,这样cindyelf或者tobby的一个操作都可能影响到对方。打个比方,如果我们通过cindyelf.raiseSalary()方法改变了salary域的值,那么tobby通过getSalary()方法得到的就是修改之后的salary域的值,显然这不是我们愿意看到的。我们希望得到tobby的一个精确拷贝,同时两者互不影响,这时候我们就可以使用Clone来满足我们的需求。Employee cindy=tobby.clone(),这时会生成一个新的Employee对象,并且和tobby具有相同的属性值和方法。

②Shallow Clone&Deep Clone

​ Clone是如何完成的呢?Object在对某个对象实施Clone时对其是一无所知的,它仅仅是简单地执行域对域的copy,这就是Shallow Clone。这样,问题就来了咯,以Employee为例,它里面有一个域hireDay不是基本型别的变量,而是一个reference变量,经过Clone之后就会产生一个新的Date型别的reference,它和原始对象中对应的域指向同一个Date对象,这样克隆类就和原始类共享了一部分信息,而这样显然是不利的,过程下图所示:

img

​ 这个时候我们就需要进行deep Clone了,对那些非基本型别的域进行特殊的处理,例如本例中的hireDay。我们可以重新定义Clone方法,对hireDay做特殊处理,如下代码所示:

   class Employee implements Cloneable {  
        public Object clone() throws CloneNotSupportedException {  
         	Employee cloned = (Employee) super.clone();  
     		cloned.hireDay = (Date) hireDay.clone()  
      		return cloned;  
        }  
} 

③Clone()方法的保护机制

​ 在Object中Clone()是被申明为protected的,这样做是有一定的道理的,以Employee

​ 类为例,通过申明为protected,就可以保证只有Employee类里面才能“克隆”Employee对象,原理可以参考我前面关于public、protected、private的学习笔记。

④Clone()方法的使用

​ Clone()方法的使用比较简单,注意如下几点即可:

​ a. 什么时候使用shallow Clone,什么时候使用deep Clone,这个主要看具体对象的域是什么性质的,基本型别还是reference variable

​ b. 调用Clone()方法的对象所属的类(Class)必须implements Clonable接口,否则在调用Clone方法的时候会抛出CloneNotSupportedException。

#### 4、通过反序列化机制

①为什么要进行序列化

​ 再介绍之前,我们有必要先了解下对象的生命周期,我们知道Java中的对象都是存在于堆内存中的,而堆内存是可以被垃圾回收器不定期回收的。从对象被创建到被回收这一段时间就是Java对象的生命周期,也即Java对象只存活于这个时间段内。

​ 对象被垃圾回收器回收意味着对象和对象中的成员变量所占的内存也就被回收,这意味着我们就再也得不到该对象的任何内容了,因为已经被销毁了嘛,当然我们可以再重新创建,但这时的对象的各种属性都又被重新初始化了。所以如果我们需要保存某对象的状态,然后再在未来的某段时间将该对象再恢复出来的话,则必须要在对象被销毁即被垃圾回收器回收之前保存对象的状态。要保存对象状态的话,我们可以使用文件、数据库,也可以使用序列化,这里我们主要介绍对象序列化。我们很有必要了解这方面的内容,因为对象序列化不仅在保存对象状态时可以被用到(对象持久化),在Java中的远程方法调用RMI也会被用到,在网络中要传输对象的话,则必须要对对象进行序列化,关于RMI有机会我会再专门开贴介绍。

​ 简单总结起来,进行对象序列化的话的主要原因就是实现对象持久化和进行网络传输,这里先只介绍怎样通过对象序列化保存对象的状态。

​ 下面我们通过一个简单的例子来介绍下如何进行对象序列化。

②怎样进行对象序列化

​ 假设我们要保存Person类的某三个对象的name、age、height这三个成员变量,当然这里只是简单举例

​ 我们先看下Person类,要序列化某个类的对象的话,则该类必要实现Serializable接口,从Java API中我们发现该接口是个空接口,即该接口中没声明任何方法。

import java.io.Serializable;  
public class Person implements Serializable {  
        int age;  
        int height;  
        String name;  
        public Person(String name, int age, int height){  
            this.name = name;  
            this.age = age;  
            this.height = height;  
    	}  
}  

​ 下面我们看一下如何来进行序列化,这其中主要涉及到 Java 的 I/O 方面的内容,主要用到两个类 FileOutputStream 和 ObjectOutputStream , FileOutputStream 用于将字节输出到文件, ObjectOutputStream 通过调用 writeObject 方法将对象转换为可以写出到流的数据。所以整个流程是这样的: ObjectOutputStream 将要序列化的对象转换为某种数据,然后通过 FileOutputStream 连接某磁盘文件,再对象转化的数据转化为字节数据再将其写出到磁盘文件。下面是具体代码:

import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.ObjectOutputStream;  
public class MyTestSer {  
/** 
 * Java对象的序列化与反序列化 
 */  
public static void main(String[] args) {  
    Person zhangsan = new Person("zhangsan", 30, 170);  
    Person lisi = new Person("lisi", 35, 175);  
    Person wangwu = new Person("wangwu", 28, 178);  
    try {  
        //需要一个文件输出流和对象输出流;文件输出流用于将字节输出到文件,对象输出流用于将对象输出为字节  
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));  
        out.writeObject(zhangsan);  
        out.writeObject(lisi);  
        out.writeObject(wangwu);  
        out.close();  
        } catch (IOException e) {  
        	e.printStackTrace();  
        }  
    }  
}  

③对象的反序列化

​ 我们存储的目的主要是为了再恢复使用,下面我们来看下加上反序列化后的代码:

import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
public class MyTestSer {  
/** 
 * Java对象的序列化与反序列化 
 */  
    public static void main(String[] args) {  
        Person zhangsan = new Person("zhangsan", 30, 170);  
        Person lisi = new Person("lisi", 35, 175);  
        Person wangwu = new Person("wangwu", 28, 178);  
        try {  
            //需要一个文件输出流和对象输出流;文件输出流用于将字节输出到文件,对象输出流用于将对象输出为字节  
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));  
            out.writeObject(zhangsan);  
            out.writeObject(lisi);  
            out.writeObject(wangwu);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        try {  
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));  
            Person one = (Person) in.readObject();  
            Person two = (Person) in.readObject();  
            Person three = (Person) in.readObject();  
            System.out.println("name:"+one.name + " age:"+one.age + " height:"+one.height);  
            System.out.println("name:"+two.name + " age:"+two.age + " height:"+two.height);  
            System.out.println("name:"+three.name + " age:"+three.age + " height:"+three.height);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}  

​ 输出结果如下:

name:zhangsan age:30 height:170  
name:zhangsan age:35 height:175  
name:zhangsan age:28 height:178  

​ 从添加的代码我们可以看到进行反序列化也很简单,主要用到的流是FileInputstream和ObjectInputstream正好与存储时用到的流相对应。另外从结果顺序我们可以看到反序列化后得到对象的顺序与序列化时的顺序一致。

④总结

​ 进行对象序列化主要目的是为了保存对象的状态(成员变量)。

​ 进行序列化主要用到的流是FileOutputStream和ObjectOutputStream。FileOutputStream主要用于连接磁盘文件,并把字节写出到该磁盘文件;ObjectOutputStream主要用于将对象写出为可转化为字节的数据。

​ 要将某类的对象序列化,则该类必须实现Serializable接口,该接口仅是一个标志,告诉JVM该类的对象可以被序列化。如果某类未实现Serializable接口,则该类对象不能实现序列化。

​ 保存状态的目的就是为了在未来的某个时候再恢复保存的内容,这可以通过反序列化来实现。对象的反序列化过程与序列化正好相反,主要用到的两个流是FileInputstream和ObjectInputStream。

​ 反序列化后得到的对象的顺序与保存时的顺序一致。

⑤补充

​ 补充一:上面我们举得例子很简单,要保存的成员变量要么是基本类型的要么是String类型的。但有时成员变量有可能是引用类型的,这是的情况会复杂一点。那就是当要对某对象进行序列化时,该对象中的引用变量所引用的对象也会被同时序列化,并且该对象中如果也有引用变量的话则该对象也将被序列化。总结说来就是在序列化的时候,对象中的所有引用变量所对应的对象将会被同时序列化。这意味着,引用变量类型也都要实现Serializable接口。当然其他对象的序列化都是自动进行的。所以我们只要保证里面的引用类型是都实现Serializable接口就行了,如果没有的话,会在编译时抛出异常。如果序列化的对象中包含没有实现Serializable的成员变量的话,这时可以使用transient关键字,让序列化的时候跳过该成员变量。使用关键字transient可以让你在序列化的时候自动跳过transient所修饰的成员变量,在反序列化时这些变量会恢复到默认值。

​ 补充二:如果某类实现了Serializable接口的话,其子类会自动编程可序列化的,这个好理解,继承嘛。

​ 补充三:在反序列化的时候,并不会调用对象的构造器,这也好理解,如果调用了构造器的话,对象的状态不就又重新初始化了吗。

​ 补充四:我们说到对象序列化的是为了保存对象的状态,即对象的成员变量,所以静态变量不会被序列化。

十二、有没有可能两个不相等的对象有相同的hashcode

​ 有可能.在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般有以 下几种方式来处理:

拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被 分配到同一个索引上的多个节点可以用这个单向链表进行存储.

开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找 到,并将记录存入

再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算 地址,直到无冲突.

十三、final

​ final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了:

被final修饰的类不可以被继承

被final修饰的方法不可以被重写

被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.

被final修饰的方法,JVM会尝试将其内联,以提高运行效率

被final修饰的常量,在编译阶段会存入常量池中.

​ 注意第三点,无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类或传址的对象来说,被 final 修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。

​ 除此之外,编译器对final域要遵守的两个重排序规则更好: 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之 间不能重排序

​ 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序.

十四、static

​ 字面上,意思是静态的,一旦被static修饰,说明被修饰的对象在一定范围内是共享的,这时候需要注意并发读写的问题。

1、修饰类成员

​ static 修饰类成员时,如何保证线程安全是我们常常需要考虑的。当多个线程同时对共享变量进行读写时,很有可能会出现并发问题,如我们定义了:public static List list = new ArrayList();这样的共享变量。这个 list 如果同时被多个线程访问的话,就有线程安全的问题,这时候一般有两个解决办法:

​ 把线程不安全的 ArrayList 换成 线程安全的 CopyOnWriteArrayList;

​ 每次访问时,手动加锁。

2、修饰方法

​ 当 static 修饰方法时,代表该方法和当前类是无关的,任意类都可以直接访问(如果权限是 public 的话)。

​ 有一点需要注意的是,该方法内部只能调用同样被 static 修饰的方法,不能调用普通方法。

​ 我们常用的 util 类里面的各种方法,我们比较喜欢用 static 修饰方法,好处就是调用特别方便,无需每次new出一个对象。

​ static 方法内部的变量在执行时是没有线程安全问题的。方法执行时,数据运行在栈里面,栈的数据每个线程都是隔离开的,所以不会有线程安全的问题。

3、修饰代码块

​ 当 static 修饰方法块时,我们叫做静态代码块,静态代码块常常用于在类启动之前,初始化一些值。

public calss PreCache{
    static{
        //执行相关操作
    }
}

十五、a=a+b与a+=b

​ += 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型, 而a=a+b则不会自动进行类型转换.如:

byte a = 127;
byte b = 127;
b = a + b; // 报编译错误:cannot convert from int to byte
b += a;

​ 以下代码是否有错,有的话怎么改?

short s1= 1;
s1 = s1 + 1;

​ 有错误.short类型在进行运算时会自动提升为int类型,也就是说 s1+1 的运算结果是int类型,而s1是short 类型,此时编译器会报错. 正确写法:

short s1= 1;
s1 += 1;
+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错.

十六、try catch finally,try里有return,finally还执行么

​ 执行,并且finally的执行早于try里面的return

​ 1、不管有木有出现异常,finally块中代码都会执行;

​ 2、当try和catch中有return时,finally仍然会执行;

​ 3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保 存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是 在finally执行前确定的;

​ 4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

十七、 Exception与Error

Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常 (RuntimeException),错误(Error)。
1、运行时异常

​ 定义:RuntimeException及其子类都被称为运行时异常。

​ 特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明 抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的 ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fast机制产生 的ConcurrentModificationException异常(java.util包下面的所有的集合类都是快速失败的,“快速失 败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作 时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程 2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的 修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制,这个错叫并发修改异常。Fail-safe, java.util.concurrent包下面的所有的类都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容 变化了,迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了 变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。 ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一 致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。)等,都属于运 行时异常。

​ 常见的五种运行时异常:

​ ClassCastException(类转换异常)

​ IndexOutOfBoundsException(数组越界)

​ NullPointerException(空指针异常)

​ ArrayStoreException(数据存储异常,操作数组是类型不一致)

​ BufferOverflowException

2、被检查异常

​ 定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。

​ 特点 : Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处 理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口 去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出 CloneNotSupportedException异常。被检查异常通常都是可以恢复的。

​ 如:

​ IOException

​ FileNotFoundException

​ SQLException

​ 被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的 FileNotFoundException 。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引 用时没有确保对象非空而引起的 NullPointerException 。

#### 3、错误

​ 定义 : Error类及其子类。

​ 特点 : 和运行时异常一样,编译器也不会对错误进行检查。

​ 当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这 些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。 OutOfMemoryError、ThreadDeath。

​ Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等

十八、线程、程序和进程

​ 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线 程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程, 或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

​ 程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

​ 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是 一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个 指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输 入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。

​ 线程是进程划分 成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同 一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间 内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

线程上下文的切换比进程上下文切换要快很多

  • 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。
  • 线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。

十九、线程的基本状态

​ 新建(new):新创建了一个线程对象。

​ 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。

​ 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。

​ 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:

​ (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waiting queue)中。

​ (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步 锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

​ (三). 其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

​ 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

​ 备注: 可以用早起坐地铁来比喻这个过程(下面参考自牛客网某位同学的回答):

​ 还没起床:sleeping
​ 起床收拾好了,随时可以坐地铁出发:Runnable
​ 等地铁来:Waiting
​ 地铁来了,但要排队上地铁:I/O阻塞
​ 上了地铁,发现暂时没座位:synchronized阻塞
​ 地铁上找到座位:Running
​ 到达目的地:Dead

二十、多线程

​ 多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。

#### 1、多线程的必要性

​ 1、使用线程可以把占据长时间的程序中的任务放到后台去处理。

​ 2、用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。

​ 3、程序的运行速度可能加快。

#### 2、三种使用方法

①继承Thread类

MyThread.java

public class MyThread extends Thread {
	@Override
	public void run() {
		super.run();
		System.out.println("MyThread");
	}
}
Run.java

public class Run {
	public static void main(String[] args) {
		MyThread mythread = new MyThread();
		mythread.start();
		System.out.println("运行结束");
	}
}

②实现Runnable接口

​ 推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。

MyRunnable.java

public class MyRunnable implements Runnable {
	@Override
	public void run() {
		System.out.println("MyRunnable");
	}
}
Run.java

public class Run {
	public static void main(String[] args) {
		Runnable runnable=new MyRunnable();
		Thread thread=new Thread(runnable);
		thread.start();
		System.out.println("运行结束!");
	}
}

③使用线程池

​ 在《阿里巴巴Java开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。

​ 为什么呢?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

​ 另外《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
3、分类

用户线程

​ 运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程

守护进程

​ 运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 “佣人”。

  • 特点: 一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作
  • 应用: 数据库连接池中的检测线程,JVM虚拟机启动后的检测线程
  • 最常见的守护线程: 垃圾回收线程

如何设置守护进程

​ 可以通过调用 Thead 类的 setDaemon(true) 方法设置当前的线程为守护线程。

注意:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑

二十一、sleep()和wait()

​ 两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁 。

​ 两者都可以暂停线程的执行。

​ Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。

​ wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。

为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
这是另一个非常经典的java多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

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

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

二十二、IO流

​ 按照流的流向分,可以分为输入流和输出流;

​ 按照操作单元划分,可以划分为字节流和字符流;

​ 按照流的角色划分为节点流和处理流。

​ Java Io 流共涉及 40 多个类,从如下 4 个抽象类基类中派生出来的。

​ InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。

​ OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

​ 分类结构图:
在这里插入图片描述

数据结构与算法

一、数组

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值