java基本

1:反射

在运行时动态的获取信息以及动态调用对象的方法的功能称为Java 的反射机制。

Class类对象和类的对象区别:

类对象应该指类Class对象,字节码对象可以通Class.forName()/getclass()/.class来获取,当jvm加载一个类时就会为这个类创建一个Class对象。
类的对象,通常就是指我们通过new这个类或者反射得到Class对象再调newInstance()创建的对象,存在内存的堆中,也叫类的实例。

获取Class字节码对象后,调用newInstance()有两种方式。

1:操作无参的构造函数实现方式

Class字节码对象.newInstance();

2:操作有参的构造函数实现方式

Constructor constructor =Class字节码对象.getConstructor(String.class);

constructor.newInstance("String类型");

Class.forName()和ClassLoader.loadClass的区别:

Class.forName(className)方法,内部实际调用的方法Class.forName(className,true,classloader);第2个boolean参数表示类是否需要初始化,Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。

ClassLoader.loadClass(className)方法,内部实际调用的方法是  ClassLoader.loadClass(className,false);第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以知道不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行


2:String类

  • equals() 与 == 的区别是什么?

对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;

    如果作用于引用类型的变量,则比较的是所指向的对象的地址

对于equals方法,equals方法不能作用于基本数据类型的变量

    如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。

通过equals()比较两个对象时返回true它们的hashCode()值一定相同。因此在重写equals方法的时候,同时也需要重写hashcode。以维持这一特性。

hashCode()值相同通过equals()比较两个对象时返回不一定是true

Equals的特性:自反性,对称性,传递性,一致性,非空性。

  • Java的String,StringBuffer,StringBuilder有什么区别?

String是不可变类(immutable),每次在String对象上的操作都会生成一个新的对象;StringBuffer和StringBuilder则允许在原来对象上进行操作,而不用每次增加对象;StringBuffer是线程安全的,但效率较低,而StringBuilder则不是效率最高。

当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

StringBuilder为什么线程不安全?

StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。所以,缓存冲这也是对 StringBuffer 的一个优化吧,不过 StringBuffer 的这个toString 方法仍然是同步的。

  • String与char[]数组之间的关系

两者的转化:

将字符串转为字符(char)数组 :String类的toCharArray()方法

将字符(char)数组转换为字符串 :String类的valueOf()方法

  • String为什么设计成不可变

字符串常量池的需要 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。

允许String对象缓存HashCode字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码.

安全性String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。

线程同步由于 String 是不可变的,它可以安全地共享许多线程,这对于多线程编程非常重要. 并且避免了 Java 中的同步问题,不变性也使得String 实例在 Java 中是线程安全的,这意味着你不需要从外部同步 String 操作。关于 String 的另一个要点是由截取字符串 SubString 引起的内存泄漏,这不是与线程相关的问题,但也是需要注意的。

  • 当一个String实例调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用。

3:jdk1.7和1.8区别

Jvm运行时的数据区域划分:

最大的差别就是:元数据区取代了永久代。元空间的本质和永久代(方法区)类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。 

接口的默认和静态方法

通过default关键字允许在接口中定义一个默认方法(普通方法)

使用方式:创建子类对象.方法名

若一个子类继承一个父类并且实现了一个接口。这个父类和接口都同时定义了一个相同名称的方法(接口中的默认方法,父类的普通方法)。则子类调用时以父类为准,即类优先原则。

若一个接口定义了一个默认方法,另外一个接口也定义了一个同名的默认方法。当一个类同时实现这两个接口的时候,会提示要自重写默认方法,否则报错。

CurrentHashMap:1.7:分段锁,1.8:CAS;Hashmap:1.7:底层是数组+链表;链表上新增元素采用头插法,1.8:底层是数组+链表+红黑树;链表上新增元素采用尾插法

Switch语句支持String类型

函数式接口:只有一个抽象方法的接口。当有两个或两个以上的抽象方法,或没有抽象方法都不可以叫做函数式接口。但是函数式接口中可以含有默认方法,静态方法、被显示重写的Object类的public方法,即需要加上@override(除了clone是protected外,其余八个都是public),只要求抽象方法的个数为1个一般带有@FunctionalInterface标识。

1.8之前常用的函数式接口:runnablle,callable,comparable

1.8新增的函数式接口:

Lambda表达式 :本质上是一段匿名内部类使用lambda表达式的一个前提要求是,该变量必须实现某个函数式接口。有三种写法。()可以大致理解为就是函数式接口的唯一抽象方法,里面是该抽象方法的参数变量(可以写参数类型,也可以不写参数类型)

(参数)->单行语句

(参数)->{多行语句}

(参数)->表达式

方法与构造函数引用使用 :: 关键字来传递方法或者构造函数引用若lambda体中的内容有其他方法已经实现了,那么可以使用“方法引用”即直接调用这个方法使用就行 也可以理解为方法引用是lambda表达式的另外一种表现形式并且其语法比lambda表达式更加简单

java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回 Stream 本身,这样就可以将多个操作依次串起来。Stream的创建需要指定一个数据源,比如 java.util.Collection 的子类,List 或者 Set,但是Map不支持。Stream的操作可以串行执行stream()或者并行执行parallelStram()。

第一步:创建stream:

Collection提供了两个方法.stream()(串行执行)与paralleStream()(并行执行,依赖fork-join框架,不需要自己创建多线程)

通过Arrays中的Stream()获取一个数组流。

通过Stream类中静态方法of()

第二步:多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理,而在终止操作时一次性全部执行,称为“惰性求值

中间操作:

  *  filter-接收lambda,从流中排除某些元素

  *  limit-接收integer,截断流,使其元素个数不超过给定的integer数量

  *  distinct-一个去除重复元素的方法

  *  map-接收lambda,将元素转换为其他形式或提取信息时,接收一个函数作为参数,该函数被应用到每个元素上,并将其映射成一个新的元素

  *  sort-排序方法

第三步:终止操作:

*      查找和匹配

         *  allMatch-检查是否匹配所有元素

         *  anyMatch-检查是否至少匹配一个元素

         *  noneMatch-检查是否没有匹配所有元素

         *  findFirst-返回第一个元素

         *  findAny-返回当前流中的任意元素

         *  count-返回流中元素的总个数

         *  max-返回流中最大值

         *  min-返回流中最小值

 *  reduce归约操作: reduce:(T identity,BinaryOperator)-可以将流中元素反复结合起来,得到一个值

   *  collect收集操作:将流转为其他形式,用于给Stream中元素做汇总

//.collect(Collectors.toList()));//.collect(Collectors.toMap(item -> item.getPid(),


4:常见的集合

集合之间的关系

Iterator接口是迭代器接口,主要用于遍历集合中的元素,但是不能遍历Map集合,只能遍历Collection接口的实现类。

再看Enumeration,它是JDK 1.0引入的抽象类。作用和Iterator一样,也是遍历集合;但是Enumeration的功能要比Iterator少,不能支持删除操作,且不再fail-fast机制。在上面的框图中,Enumeration只能在Hashtable, Vector, Stack中使用。

Arrays和Collections工具类,是用来操作数组、集合的两个工具类。

FAIL-FAST和FAIL-SAFE:

FAIL-FAST原理:

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用modCount变量,

集合中在被遍历期间如果内容发生变化,就会改变modCount的值,

每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测modCount变量和expectedmodCount值是否相等,

如果相等就返回遍历,否则抛出异常,终止遍历.

FAIL-SAFE原理:

由于迭代时是对原集合的拷贝的值进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会出发ConcurrentModificationException

使用场景:

java.util包下的集合类都是快速失败机制的, 不能在多线程下发生并发修改(迭代过程中被修改).

java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改.

快速失败和安全失败是对迭代器而言的。并发环境下建议使用 java.util.concurrent 包下的容器类,除非没有修改操作

For循环删除list中元素的注意事项:

普通for循环

 这种方式的问题在于,删除某个元素后,list的大小发生了变化,而索引也在变化,所以会导致在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。解决办法第一种是手动将要访问的元素也同时向前移动一位。第二种是逆序遍历list然后再进行删除

增强For循环

这种方式的问题在于,删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出。在用迭代器迭代集合的时候,迭代器一旦创建,就不允许更改集合,如果所迭代的集合(Set或者List)的有修改的话,就会抛出 ConcurrentModificationException异常, 用迭代器自身的remove方法除外。在 foreach 循环中执行 list.remove(item);,对 list 对象的 modCount 值进行了修改,而 list 对象的迭代器的 expectedModCount 值未进行修改,因此抛出了ConcurrentModificationException异常。

Iterator迭代器遍历

这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。

Hashmap:允许null值和null键,但是null键只允许存在一个,多个null键的时候会发生值覆盖,,null值是允许多个。不保证顺序,非线程安全

容量capacity默认16,容量capacity最大值为2^30,加载因子loadfactor默认0.75,链表转化为红黑树的节点数treeify-threshold默认8,红黑树转化为链表的节点数untreeify-threshold默认6,树的最小节点数量min-tree-capacity默认64(只要容量capacity大于64时才真正的进行链表向红黑树的转变,否则和jdk1.7一样只进行数组扩容),底层数据结构Node(含hash,key,value,下一个节点Node),threshold=capacity*loadfactor=12.

构造函数:若是用户指定的初始容量大于capacity最大值,则将最大值修改为用户指定的值;根据用户传入的初始容量和加载因子计算threshold;

Hashmap线程不安全的原因(造成环):

当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作。具体见https://blog.csdn.net/hhx0626/article/details/54024222

Get:

1:先根据传进来的key,得到该key在数组中的角标。这个过程主要分为三步

在HashMap中,计算key的hash值,也就是在table数组中的下标位置算法很巧妙,用到了许多位运算。

主要分为三步:

  1. 计算对象自己的hashCode()值
  2. 计算上步得到的值进行无符号右移16位,然后和上步计算的hashcode值进行异或这样做的好处有2点:

第一:由于n比较小,导致hash只要低4位参与了运算,高位的计算可认为是无效的,这样就导致计算结果只和底位信息相关,高位数据没有发挥作用。通过这种方式,让高位数据和低位数据进行异或,以此增加低位信息的随机性,变相的让高位数据参与了运算

第二:增加hash的复杂度,避免因重写的hashCode导致分布不佳。

3.定位操作,对上步计算得到hash值进行取模操作(这里用的是位运算)

   

 

2:若角标为空直接返回null;

3:若角标不为空,检查该角标对应的第一个节点,若hash值不同直接返回null,若相同则进行key的equals的判断两个键是否相同(这也是为什么重写equals要求重写hashcode的原因:因为重写的equals一般比较的比较全面和复杂,这样效率会很低。而利用hashcode()进行比对时,只需要生成一个hash值,首先通过hash进行判等操作,若hash值不等直接返回,这样大大的加快了判断速度;而不同的键可能产生相同的hash,因此仅仅凭借hash是不够完全准确,所有hash值相同的情况下还要进行一次equals操作。这种比较机制常用在hash容器,比如hashmap,hashtable,hashset等)。若通过equals判断后仍然相等的话,则说明第一个节点就是要查找的Node,返回该Node。若equals不同则对第一个节点的Next进行判断;

4:若第一个节点的Next存在则要分两种情况,因为在1.8下,同一角标上的多个Node有链表和红黑树两种解决hash冲突的方法。用instanceof判断第一个节点的next节点是不是TreeNode类型,若是按照红黑树的方法查找,若不是则进行单向链表的遍历(do-while实现)。两种情况下要是找到对应的Node返回该Node。否则返回null。

Put:(对于增,删,改在操作完成都会++modCount操作,该字段主要用于Fail-Fast ,在这些线程不安全的集合中,初始化迭代器时会给这个modCount赋值,如果在遍历的过程中,一旦发现这个对象的modCount和迭代器存储的modCount不一样,就会报错。因此推荐集合遍历的时候采用迭代器)

1:首先当数组为null或者数组长度为0时,要进行一次扩容(即此次扩容是给数组赋初始长度和阈值。初始扩容和增大扩容是写在一个resize()方法中,因此该方法前面的一系列判断就是为了区分是初始扩容还是增量扩容)。即将底层的数据结构延迟到插入键值对时再进行初始化

2: 然后根据传进来的key,调用重写的静态hash方法得到哈希值,将哈希值与数组长度减1进行按位与(这里就要求数组table的值是2的整数幂,目的是尽量散列均匀,减少哈希碰撞)得到该key在数组中的角标。

3:若角标为null,说明数组此角标位置上现在没有Node,则此时直接建立一个新的节点,将该角标指向它

4:若角标不为空,则先判断第一个Node节点,若hash和equals()都通过的话则让e指向和它相等的节点。若第一个节点和现有Node不等,则需要判断是用红黑树增加节点还是单链法增加节点。

5:用instanceof判断第一个节点是不是TreeNode类型,若是按照红黑树的方法增加,若不是TreeNode,则开始对这个位置上的链表进行遍历。在遍历的过程中通过binCount记录键值对个数,当遍历到链表的末尾的时候生成新的节点插入,但是要根据binCount判断此时是否需要进行红黑树的转化,同时若在遍历的过程中发现和插入节点相等的节点则直接跳出循环,让e指向和他相等的节点

6:判断e执行的节点是否为null。不为null的时候表示集合中存在和插入元素相等的键值对,则用新值覆盖旧值,并将旧值返回。

7:最后还需要进行一次判断。整个操作完成后,++size,size代表整个map中键值对的数量。当size>threshold的时候要进行一次扩容操作。size指的是整个集合中键值对的数量,也就意味着长度为16的数组当存储的键值对个数大于12个的时候就要进行第一次扩容。要想达到红黑树的转变至少需要扩容2次才能满足数组的大小不小于64.

Resize:

1:计算新桶的容量newCap和新阈值newThr

2:根据计算出的newCap创建新的桶数组,上面的put过程中桶数组也是在这里进行初始化的

3:将键值对节点重新映射到新的桶数组中。对于树形节点,需要先拆分成红黑树再映射,对于链表节点类型,则需要先对链表进行分组,然后再映射,且分组后,组内节点相对位置保持不变。依次遍历链表,并计算hash&oldCap的值,如果值为0,将 loHead 和 loTail 指向这个节点。如果后面还有节点 hash & oldCap 为0的话,则将节点链入 loHead 指向的链表中,并将 loTail 指向该节点。如果值为非0的话,则让 hiHead 和 hiTail 指向该点。完成遍历后,可能会得到两条链表(最坏的时候一个桶节点对应的链表上hash & oldCap都为0或者非0),此时就完成了链表分组。

 

 

 

Remove

1:首先也是根据键定义到桶的位置(三步定hash),若该角标处的第一个Node与其相等(包括hash和key),则就取该Node

2:若第一个不满足,则按照红黑树或者链表的查找方法找到符合条件的Node

3:按照红黑树或者链表的方式将该节点删除。同时要进行-链表或者红黑树修复操作。

TreeifyBin(将普通节点链表转化为树形节点链表)

1:树化的前提条件是:桶节点中的链表长度大于等于8,桶数组容量大于等于64.当桶数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。毕竟高碰撞率是因为桶数组容量较小引起的,这个是主因。容量小时,优先扩容可以避免一些不必要的树化过程。同时,桶容量较小时,扩容会比较频繁,扩容时需要拆分红黑树并重新映射。所以在桶容量比较小的情况下,将长链表转成红黑树是一件吃力不讨好的事。

2:首先将普通链表转化为TreeNode型节点组成的链表,然后调用treeify将该链表转化为红黑树。TreeNode继承Node,因此TreeNode仍然包含next引用,原链表的节点顺序最终通过next引用保存下来。

视图:

支持三种视图模式

父类AbstractMap:

    transient Set<K>         keySet; // key view

    transient Collection<V>   values; // value view

子类HashMap:

    transient Set<Map.Entry<K,V>>  entrySet;

加载因子设置成0.75的原因:

加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。

引入红黑树的原因:

当个数不多的时候,直接链表遍历更方便,实现起来也简单。而红黑树的实现要复杂的多,要进行左旋,右旋等操作以维持平衡。【参考知识点十六】

解决哈希冲突的方法:拉链法,再哈希法,开放定址法,建立公共溢出区。

底层数据结构table被定义为transient的原因:

被transient修饰的变量是不会被默认的序列化机制序列化。

第一因为hashmap存储的是键值对,所以只要把键值对序列化,就可以根据键值对重构hashmap。

第二因为table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间;

第三因为同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。

Hashtable:线程安全(通过synchronized),不允许null键和null值;无序

视图:支持Enumeration迭代器访问

synchronized Set<K>               keySet()

synchronized Collection<V>       values()

synchronized Set<Entry<K, V>>    entrySet()

synchronized Enumeration<V>      elements()

synchronized Enumeration<K>      keys()

Hashmap和Hashtable的比较

1:hashTable是线程安全,不支持null键和null值,hashmap非线程安全,支持null键(只能有一个)和null值(可以有多个)

 HashMap计算key的hash值时调用单独的方法,在该方法中会判断key是否为null,如果是则返回0;而Hashtable中则直接调用key的hashCode()方法,因此如果key为null,则抛出空指针异常。

  HashMap将键值对添加进数组时,不会主动判断value是否为null;而Hashtable则首先判断value是否为null。

ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。

2:HashTable中hash数组默认大小是11,扩容大小是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数,扩容大小是old*2。

3:hashtable支持Enumeration的遍历,该遍历方式是非fail-fast的

4:因为hashmap非线程安全所有速度较快

5:hashtable和hashmap取hash和定义角标

Hashtable:

Hashmap:

 

Treemap:非线程安全,有序,基于红黑树(红黑树的参考知识点十六)

LinkedHashMap:有顺序的存储

插入有序:put的时候的顺序是什么,取出来的时候就是什么样子

访问排序get的时候,会改变元素的顺序,会把该元素移到数据的末尾

访问排序的实现:创建集合的时候指定为true

两种比较实现方式的比较:

①、Comparator位于包java.util下,而Comparable位于包java.lang下。

②、Comparable接口将比较代码嵌入需要进行比较的类的自身代码中,而Comparator接口在一个独立的类中实现比较。

③、如果前期类的设计没有考虑到类的Compare问题而没有实现Comparable接口,后期可以通过Comparator接口来实现比较算法进行排序,并且为了使用不同的排序标准做准备,比如:升序、降序。

④、Comparable接口强制进行自然排序,而Comparator接口不强制进行自然排序,可以指定排序顺序。

数组排序:Arrays.sort(array);//会检查数组个数大于286且连续性好就使用归并排序,若小于47使用插入排序其余情况使用快速排序

集合排序:对于Integer、String等,这些类都已经重写了Compare方法,都有默认排序规则,例如对于Integer类型会比较其包装的值类型大小,对于String类型会以长度最小字符串为基准,逐一比较相同位置字符的ASCII码大小,如果都相同则比较字符串的长度。

集合排序:对于自定义类,有两种实现方式

实现compare接口,重写compareTo方法,让该类的对象自身具有比较性

实现comarator接口,重写compare方法,在调用sort方法时,传入一个比较器

Arraylist::基于动态数组实现,非线程安全,默认10,允许null值;

Add:

1:首先将size+1,产生一个minCapacity的变量,调用ensureCapacity(minCapacity)方法保证数组在插入一个元素时有可用的容量,然后将元素e放到数组elementData的size位置,即新增的元素总是位于最后一个,最后将size+1

2:调用ensureCapacity(minCapacity)时进行判断,必要时进行数组的扩容首先将modCount+1,modeCount表示修改数组结构的次数(维护在父类AbstractList中),如果入参minCapacit大于目前数组elementData的容量,则将容量扩展到 (oldCapacity * 3)/2 若此时容量仍然小于minCapacity,则直接将minCapacity设置为最新的容量最后使用Arrays.copyof()方法将原来elementData中元素copy到新的数组中,并将新数组赋值给elementData.即当扩容后的容量任然不满足的时候,就直接使用现在的长度作为数组最新长度

3:若是插入到指定角标index的位置上的时候则在前两步的基础上做一定的补充。首先在进行第一步的时候会判断角标是否越界(小于0或者大于size),然后执行第一步。在进行是否扩容的条件判断之后,要将指定角标以后的元素(从index+1到size-index)全部向后移动,然后将新元素插入到指定角标位置index。

Remove:

若不指定角标,此时需要先判断是否为null。然后根据值找到对应的角标位置,然后进行数组的复制操作。若指定角标的时候则直接进行复制操作。即删除的操作全部要转化为对角标的操作。

遍历方式

增强for;迭代器;随机访问,通过索引值

Linkedlist::基于双向链表,非线程安全,允许null

Get:效率较低,当目标index < 双向链表长度的1/2,则从前先后查找 否则,从后向前查找。

Vector:矢量队列等价于ArrayList,但它是线程安全的,默认10,但是不支持序列化

Stack:栈,线程安全

Arraylist和LinkedList区别

1.底层实现:ArrayList内部是数组实现,而LinkedList内部实现是双向链表结构

2.接口实现:ArrayList实现了RandomAccess可以支持随机元素访问,而LinkedList实现了Deque可以当做队列使用

3.性能:新增、删除元素时ArrayList需要使用到拷贝原数组,而LinkedList只需移动指针,查找元素 ArrayList支持随机元素访问,而LinkedList只能一个节点的去遍历

5:泛型:(参数化类型)

Java的泛型是如何工作的 ? 什么是类型擦除 ?

泛型是通过类型擦除来实现的,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉.

通配符有 3 种形式。

<?>被称作无限定的通配符。

<? extends T>被称作有上限的通配符。

<? super T>被称作有下限的通配符。


6:线程的创建方式

继承Thread;实现Runnable;实现collable:具有返回值;线程池创建:【具体见知识点十四中的Executor框架】

四种方式的区别和联系:

用接口实现去创建线程是多个线程共享一个target对象,适用于多线程处理同一个资源。因为继承创建线程是重写run方法,而接口实现是实例化Thread类。

Runnable接口不能有返回值,不可以抛出异常,但Collable可以。

继承Thread类的线程类不能再继承其他父类(Java单继承决定),但是却可以实现多个接口,便于程序的扩展(一个类必须是先继承,后实现)。

实现创建线程如果要访问当前线程,则必须使用Thread.currentThread()方法。而继承创建线程可以直接用this获取当前线程。


7:接口和抽象类的区别

相同点:

两者都不可直接被实例化

两者都可以包含抽象方法(接口中的所有方法必须是抽象的,而抽象类中可以不包含抽象方法(但是包含抽象方法的类一定是抽象类))。实现接口或者是继承抽象类,都要重写他们的抽象方法(若继承抽象类的子类还是抽象类,则可以不重写抽象方法)

区别:

抽象类是对类的抽象,接口是对抽象类的抽象。抽象类主要用作代码的复用,接口主要用作事物特性的抽象

抽象类除了不可以直接被实例化外,和一般普通的类没有区别。可以有局部变量,普通方法,构造函数。而接口的成员变量只能是Public static final类型,接口中的方法只能是抽象方法。

抽象类的访问权限可以是public,protected,default,private中的任意一种,而接口的访问权限必须是public。

只可以是单继承,但是可以多实现


8:Object类的方法

GetClass:是通过反射获得Class(字节码)对象的一种方式

Equals,Hashcode:返回的是对象的哈希码值,int类型,为了提高哈希表的性能。当重写equals方法时建议重写该方法。

Wait,Notify,notifyAll:这几个方法主要用于java多线程之间的协作

toString:默认返回是:getClass().getName() + "@" + Integer.toHexString(hashCode());因此建议每一个对象都重写改方法。

Clone:默认返回的是浅克隆。

深克隆:创建一个新对象,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象原对象的修改对现有对象不产生影响。(序列化实现(必须要实现serializable接口)或者覆盖object类的clone方法)

浅克隆:创建一个新对象复制时只复制它本身和其中包含的值类型的成员变量对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。原对象的修改对现有对象产生影响。

Finalize:主要与Java垃圾回收机制有关


9:多态的实现原理

JVM 的方法调用指令有个,分别是

  Invokestatic:调用static方法(类方法)

Invokespecial:调用实例构造器方法,私有方法,父类方法

Invokesvirtual:调用对象的实例方法,根据对象的实际类型进行分派

Invokeinterface:调用接口方法,运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

Invokedynamic:运行时动态解析所引用的方法,然后再执行,支持动态类型语言

Invokestatic和invokespecial是静态绑定,由这两者调用的方法称为非虚方法invokevirtual和invokeinterface是动态绑定的,由这两者调用的称为虚方法

在Class文件中的常量中存有大量的符号引用。字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接,要求这个方法(称为非虚方法)是编译期可知,运行期不变。符合要求的就是静态方法和私有方法和实例构造器和父类方法和final修饰的方法(它不是非虚方法)。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。

符号本质上就是对内存地址的标识。CPU指令执行和内存范文,都是基于地址,倘若没有地址,CPU也就无法知道自己该执行哪条指令,该访问哪里的内存数据。可是,作为人类我们却很难直接使用二进制数,于是我们通过给地址打上标记的形式来简化我们的使用,这里给地址打上的标记也就是前面所说的符号。

重载是编译时候的多态:使用哪个重载版本,完全取决于传入参数的数量和数据类型。但编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,所以在编译阶段,javac 编译器就根据参数的静态类型决定使用哪个重载版本。覆盖/重写是运行时的多态:对于普通方法遵循的是编译看左运行看右,对于静态方法编译和运行都看左。

重写/覆盖:方法名,返回值,参数列表保持相同,子类重写时方法的访问修饰符要大于父类,异常检查类型要小于父类

重载:方法名相同,参数列表不同(参数的个数,参数的顺序,参数的类型),与返回值的类型无关,不做要求。因为函数返回值只是代表函数运行之后的一个状态,它是调用者和被调用者之间进行交流的关键,不能作为一个函数的标识。

多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同。

继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。 

接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表

重写的原则:

这里的final int prime = 31为何要选这个值呢,有以下几个原因:

1.首先31是个质数,只能被1和本身整除的数,乘上后不容易出现重复

2.其次31这个数不大也不小,不至于超出返回类型的int,也不容易在乘上后重复

3.最后就是31=32-1=2^5 -1,计算方便,向左移动5位,再减一就行了

步骤:

第一步:定义一个初始值,一般来说取17

int result = 17;

第二步:分别解析自定义类中与equals方法相关的所有字段(假如hashCode中考虑的字段在equals方法中没有考虑,则两个equals的对象就很可能具有不同的hashCode)

 

    情况一:字段a类型为boolean 则[hashCode] = a ? 1 : 0;

    情况二:字段b类型为byte/short/int/char, 则[hashCode] = (int)b;

    情况三:字段c类型为long, 则[hashCode] = (int) (c ^ c>>>32);

   情况四:字段d类型为float, 则[hashCode] = d.hashCode()(内部调用的是Float.hashCode(d), 而该静态方法内部调用的另一个静态方法是Float.floatToIntBits(d))

   情况五:字段e类型为double, 则[hashCode] = e.hashCode()(内部调用的是Double.hashCode(e), 而该静态方法内部调用的另一个静态方法是Double.doubleToLongBits(e),得到一个long类型的值之后,跟情况三进行类似的操作,得到一个int类型的值)

   情况六:引用类型,若为null则hashCode为0,否则递归调用该引用类型的hashCode方法。

    情况七:数组类型。(要获取数组类型的hashCode,可采用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + ..... + s[n-1], 该方法正是String类的hashCode实现所采用的算法)

第三步:对于涉及到的各个字段,采用第二步中的方式,将其依次应用于下式:

result = result * 31 + [hashCode];


10:instanceOf与isInstance

instanceof运算符 只被用于对象引用变量,检查左边的被测试对象 是不是 右边类或接口的 实例化。如果被测对象是null值,则测试结果总是false。

形象地:自身实例或子类实例 instanceof 自身类   返回true

例: String s=new String("javaisland");

       System.out.println(s instanceof String); //true 

Class类的isInstance(Object obj)方法,obj是被测试的对象,如果obj是调用这个方法的class或接口 的实例,则返回true。这个方法是instanceof运算符的动态等价。

形象地:自身类.class.isInstance(自身实例或子类实例)  返回true

例:String s=new String("javaisland");

      System.out.println(String.class.isInstance(s)); //true


11:异常体系

1. runtimeException子类:

    1、 java.lang.ArrayIndexOutOfBoundsException, 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
       2、java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。
        3、java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
        4、java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。

     5、java.lang.NegativeArraySizeException  数组长度为负异常

     6、java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常

   7、java.lang.SecurityException 安全性异常

     8、java.lang.IllegalArgumentException 非法参数异常

2.IOException

IOException:操作输入流和输出流时可能出现的异常。

EOFException   文件已结束异常

FileNotFoundException   文件未找到异常

Try..catch..finally的执行

try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
catch 块:用于处理try捕获到的异常。一旦某个catch捕获到匹配的异常类型,整个try..catch结束,其他catch不再执行。同时越基础的异常类型应该越放在所有catch的最后。避免特定类型的异常执行不到。
finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。

以下需要注意:

a.)当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;

b.)当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;

c.)当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;


12:序列化

 序列化: 将数据结构或对象转换成二进制串的过程

 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

 当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等,而这些数据都会以二进制序列的形式在网络上传送。序列化可以使发送方把这个Java对象转换为字节序列,然后在网络上传送;反序列化可以使接收方从字节序列中恢复出Java对象。注意:如果一个类能被序列化,那么它的子类也能够被序列化。

具体操作

第一是实现seriablize接口。这虽然是一个空接口,却是可被序列化的标志。

第二要指定一个private static final long serialVersionUID 。serialVersionUID 被称为序列化 ID,它是决定Java对象能否反序列化成功的重要因子。在反序列化时,Java虚拟机会把字节流中的 serialVersionUID 与被序列化类中的 serialVersionUID 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。

通常有三种方式:添加默认的版本序列ID;添加一个随机生成的不重复的序列化ID;使用@SuppressWarnings注解。一般推荐第一种,注解的实质也是自动生成一个随机序列化ID。当一个对象序列化完成,在反序列化之前修改了该对象的序列化ID,则无法正常反序列化。

常见问题:

  1. 如果类中的一个成员未实现可序列化接口, 会发生什么情况?

如果尝试序列化实现可序列化的类的对象,但该对象包含对不可序列化类的引用,则在运行时将引发不可序列化异常 NotSerializableException

要序列化的对象(普通属性,静态属性和transit修饰属性)

测试类:

输出结果分析

对于普通属性一旦序列化完成,在反序列化之前,对属性再次修改。也不会产生影响(name);对于静态属性序列化完成,反序列化之前进行修改,会产生影响(noseri1),因为序列化保存的是对象的状态,而被static修饰是类的状态,因此不会保存static修饰的属性;被transit修饰的属性不会进行序列化,意为临时的操作,反序列化后得到的是默认值。


13:java IO/NIO

所有的系统IO操作都分为两个阶段:等待就绪和操作。具体来说,读函数分为等待系统可读和真正的读,写函数分为等待网卡可写和真正的写。其中等待就绪的阻塞是不使用cpu的,是在空等。而真正的读操作是需要cpu的,但是这个过程非常快,可以理解为不耗时。等待就绪划分为阻塞和非阻塞(等待就绪的时候是否可以提前返回),操作划分为同步和异步(到就绪后进行实际的操作,若必须是自己去读则是同步,别人读好送入内核再通知他是异步)。常见的IO模型对比

同步阻塞IO模型(BIO):服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 在读写数据的过程中会发生阻塞现象。当用户线程发出IO请求后,内核会检查数据是否就绪,如果没有就绪就等待数据就绪,而用户线程就会处于阻塞状态。当数据就绪后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才会解除block状态。典型的阻塞IO模型例子就是data=socket.read(),如果数据没有就绪,则会一直阻塞在read方法。

同步非阻塞IO模型:当用户线程发起有个read操作后,并不需要等待,而是马上得到一个结果,如果结果是一个error时,它知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上将数据拷贝到用户线程 ,然后返回。所以事实上在非阻塞IO模型中,用户线程需要不断的询问内核数据是否准备就绪,也就是非阻塞IO不会交出cpu,而会一直占用cpu

多路复用IO模型(NIO):服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正的调用实际的IO读写操作。在该模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必要去维护这些进程和线程,并且只有真正有socket读写事件时,才会使用IO资源,所以大大减少了资源占用。该模型比非阻塞模型高效的原因是在非阻塞IO中,不断询问socket状态是通过用户线程进行的,而在多路复用IO中,轮询操作是在内核进行的,效率比用户线程高很多。另外多路复用IO模型是采用轮询的方式检测是否有事件到达,并且对到达的事件逐一进行响应。因此该模型的一个缺点就是一旦事件响应体很大,就会导致后续的事件迟迟得不到处理。

NIO和BIO的区别:

NIO处理流程:面向缓冲区,读取和写入都必须先通过缓冲区,而原始IO是面向流

通道:对原始IO流的模拟,不过这是双向的,可读可写可读写,而流是单向的

NioSocket的服务端具体处理过程:(客户端则是SocketChannel)

1:创建ServerSocketChannel并设置相应的参数。

2创建Selector并调用ServerSocketChannel的registe方法将自己注册到ServerSocketChannel。其中可以通过指定register方法的第二个参数选择特定的操作(包括请求操作accept,连接操作connect,读操作read和写操作write)

3:调用Selector的select方法等待请求

4:Selector接收到请求后使用selectedKeys返回SelectionKey集合。这一步集合的返回内容,Selector可以根据第二步设置的特定操作进行过滤,只保留符合的

5:使用SelectionKey获取channel,selector,和操作类型进行具体操作。

6:具体操作一般是由一个内部类Handler处理。Handler的处理过程用到了buffer,buffer是专门用于存储数据,有4个属性重要。Capacity:容量,limit:可以使用的上限,position:当前所操作元素所在的索引位置,mark:暂时保存position值

Select,poll,epoll的区别和联系:

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select的缺点:

1:单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024)

2:内核,用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;即每次调用select的时候都会将fd文件从用户态切换到核心态。

3:select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

4:select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

Poll的缺点:

相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

Epoll:

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

Epoll工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

Epoll实现流程:

第一步:epoll_create()系统调用,创建一个epoll句柄。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。创建红黑树,用于存储epoll_ctl传递来的socket,创建链表list,用于存储准备就绪事件。

第二步:epoll_ctl()系统调用,注册要监听的事件类型。通过此调用向红黑树中添加、删除、修改感兴趣的事件,若红黑树中存在则直接返回,不存在添加到树上,返回0标识成功,返回-1表示失败注册回调函数,当事件就绪时添加到list中。

第三步:epoll_wait()系统调用,等待事件的发生。通过此调用收集在epoll监控中已经发生的事件。若list中有数据直接返回,无数据则sleep,等到timeout之后即使链表中还是没有元素,此时依旧返回。

异步非阻塞IO模型(AIO):服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理 。只需要先发起一个请求,当接收到内核返回的成功信号时表示IO操作已经完成,可以直接使用数据了。也就是说在这个模型中用户发起read操作之后,立刻就可以开始做其他事,用户线程完全不需要知道实际的IO操作是怎样完成的。但是这个模型需要操作系统底层的支持。


14:零拷贝

定义:CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,通常用于网络传输一个文件时候以减少CPU周期和内存带宽

优点:减少不必要的cpu拷贝减少用户空间和操作系统空间的上下文切换

实现:依赖于操作系统。和java本身没有关系

传统的I/O(4次复制和4次上下文的切换)

1:JVM发出read() 系统调用。

2:OS上下文切换到内核模式(第一次上下文切换)并将数据读取到内核空间缓冲区。(第一次拷贝:hardware ----> kernel buffer)

3:OS内核然后将数据复制到用户空间缓冲区(第二次拷贝: kernel buffer ——> user buffer),然后read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)。

4:JVM处理代码逻辑并发送write()系统调用。

5:OS上下文切换到内核模式(第三次上下文切换)并从用户空间缓冲区复制数据到内核空间缓冲区(第三次拷贝: user buffer ——> kernel buffer)。

6:write系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换)。将内核空间缓冲区中的数据写到hardware(第四次拷贝: kernel buffer ——> hardware)。

通过sendfile实现零拷贝(3次复制和2次上下文的切换)

1:发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard driver ——> kernel buffer)。

2:然后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer ——> socket buffer)。

3:sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)。

带有DMA收集功能的sendfile实现零拷贝(2次复制和2次上下文的切换)

1:发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。

2:没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。

3:sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。

通过mmap实现零拷贝(3次复制和4次上下文的切换)

1:发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。

2:mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。

3:发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。

4:write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)


15:自动拆箱和装箱(享元模式)

Integer实际存储的是对象的引用,执行new的integer对象,int是直接存储数据值

1:两个new出来的integer==比较永远false:因为new生成的是两个对象,其内存地址不同

2:integer变量和int变量比较时,只要两个变量的值是相等的,则结果为true(因为包装类integer和基本数据类型int比较时,java会自动拆包为int。实质就是两个int变量比较)

3:非new出来的integer变量和new出来的integer变量==的结果是false。因为非new出来的integer变量指向的是常量池中的地址,而new出来的变量指向的是堆中的地址

4:两个非new出来的integer变量,只要范围在-128-127之间,则==的结果是true。因为指向的都是常量池中的地址,在第一次引用的时候会进行缓存,第二次再使用的时候指向的就是一个地址,而超过这个范围,不会进行缓存,相当于是重新new。


16:常见问题

1:为啥java不支持多继承

1)第一个原因是围绕钻石形继承问题产生的歧义,考虑一个类 A 有 foo() 方法, 然后 B 和 C 派生自 A, 并且有自己的 foo() 实现,现在 D 类使用多个继承派生自 B 和C,如果我们只引用 foo(), 编译器将无法决定它应该调用哪个 foo()。这也称为 Diamond 问题,因为这个继承方案的结构类似于菱形,见下图:

  A foo()    
               / \    
             /     \    
 foo() B     C foo()    
             \     /    
               \ /    
               D  foo()

即使我们删除钻石的顶部 A 类并允许多重继承,我们也将看到这个问题含糊性的一面。如果你把这个理由告诉面试官,他会问为什么 C++ 可以支持多重继承而 Java不行。嗯,在这种情况下,我会试着向他解释我下面给出的第二个原因,它不是因为技术难度, 而是更多的可维护和更清晰的设计是驱动因素, 虽然这只能由 Java 言语设计师确认,我们只是推测。维基百科链接有一些很好的解释,说明在使用多重继承时,由于钻石问题,不同的语言地址问题是如何产生的。

2)对我来说第二个也是更有说服力的理由是,多重继承确实使设计复杂化并在转换、构造函数链接等过程中产生问题。假设你需要多重继承的情况并不多,简单起见,明智的决定是省略它。此外,Java 可以通过使用接口支持单继承来避免这种歧义。由于接口只有方法声明而且没有提供任何实现,因此只有一个特定方法的实现,因此不会有任何歧义

2:为啥等待通知定义在object中而不是thread中?

1)每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的一个原因。

2) Java 是基于 Hoare 的监视器的思想。在Java中,所有对象都有一个监视器。

线程在监视器上等待,为执行等待,需要2个参数:一个线程,一个监视器(任何对象)

在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中侵入的操作都被弃用了(例如 stop 方法)。

3:为什么 char 数组比 Java 中的 String 更适合存储密码?

1)由于字符串在 Java 中是不可变的,如果你将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它. 并且为了可重用性,会存在 String 在字符串池中, 它很可能会保留在内存中持续很长时间,从而构成安全威胁。由于任何有权访问内存转储的人都可以以明文形式找到密码,这是另一个原因,你应该始终使用加密密码而不是纯文本。由于字符串是不可变的,所以不能更改字符串的内容,因为任何更改都会产生新的字符串,而如果你使用char[],你就可以将所有元素设置为空白或零。因此,在字符数组中存储密码可以明显降低窃取密码的安全风险。

2)使用 String 时,总是存在在日志文件或控制台中打印纯文本的风险,但如果使用 Array,则不会打印数组的内容而是打印其内存位置。虽然不是一个真正的原因,但仍然有道理。

4:如何实现多继承

  1. 多层继承:class A,class B extends A,class C extends B
  2. 内部类:class A{class B{},class C{}}
  3. 接口:

具体见https://blog.csdn.net/weixin_42617262/article/details/85344819

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值