JAVA基础

  • JAVA中的几种基本数据类型是什么,各自占用多少字节?

在栈中可以直接分配内存的数据是基本数据类型;引用数据类型:是数据的引用在栈中,但是它的对象在堆中。

  1. 整形
    • byte 字节类型
    • short 短整型
    • int 整数类型
    • long 长整型
  2. 浮点数
    • float 浮点类型-单精度
    • double 双精度类型
  3. 逻辑型
    • boolean 布尔型
  4. 字符型
    • char 字符型-一个字符可以存储一个汉字
类型 字节长度
byte 1
short 2
int 4
long 8
float 4
double 8
boolean 1/8
char 2
  • String类能被继承吗,为什么?

不能。String被关键字final修饰,所以不能被继承。
这里就要简单介绍一下final的基础。
1、修饰类当用final去修饰一个类的时候,表示这个类不能被继承。
注意:
a. 被final修饰的类,final类中的成员变量可以根据自己的实际需要设计为fianl。
b. final类中的成员方法都会被隐式的指定为final方法。说明:在自己设计一个类的时候,要想好这个类将来是否会被继承,如果可以被继承,则该类不能使用fianl修饰,在这里呢,一般来说工具类我们往往都会设计成为一个fianl类。在JDK中,被设计为final类的有String、System等。
2、修饰方法被final修饰的方法不能被重写。
注意:
a. 一个类的private方法会隐式的被指定为final方法。
b. 如果父类中有final修饰的方法,那么子类不能去重写。
3、修饰成员变量
注意:
a. 必须初始化值。
b. 被fianl修饰的成员变量赋值,有两种方式:1、直接赋值 2、全部在构造方法中赋初值。
c. 如果修饰的成员变量是基本类型,则表示这个变量的值不能改变。
d. 如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的。
总之final就是像C语言中的const一样,代表一个常量,你在一开始初始化就要赋值,也不允许你改变它。

  • String,Stringbuffer,StringBuilder的区别?

1、字符修改上的区别(主要)
String:不可变字符串;(栈中存的是堆的地址引用,每次 更改的是引用,而堆中会生成新的存储地址)
StringBuffer:可变字符串、效率低线程安全
StringBuilder:可变字符序列、效率高线程不安全
2、初始化上的区别,String可以空赋值,后者不行,报错

①String
StringBuffer s = null;   
StringBuffer s = “abc”;    
②StringBuffer
StringBuffer s = null; //结果警告:Null pointer access: The variable result can only be null at this location
StringBuffer s = new StringBuffer();//StringBuffer对象是一个空的对象 
StringBuffer s = new StringBuffer(“abc”);//创建带有内容的StringBuffer对象,对象的内容就是字符串”

小结:
(1)如果要操作少量的数据用 String;
(2)多线程操作字符串缓冲区下操作大量数据 StringBuffer;
(3)单线程操作字符串缓冲区下操作大量数据 StringBuilder(推荐使用)。

  • ArrayList和LinkedList有什么区别?

ArrayList和LinkedList的大致区别如下:
ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。
对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

ArrayList:因为数组在存储数据时是按顺序存储的,存储数据的内存也是连续的,所以他的特点就是寻址读取数据比较容易,插入和删除比较困难。简单解释一下为什么,在读取数据时,只需要告诉数组要从哪个位置(索引)取数据就可以了,数组会直接把你想要的位置的数据取出来给你。插入和删除比较困难是因为这些存储数据的内存是连续的,要插入和删除就需要变更整个数组中的数据的位置。举个例子:一个数组中编号0->1->2->3->4这五个内存地址中都存了数组的数据,但现在你需要往4中插入一个数据,那就代表着从4开始,后面的所有内存中的数据都要往后移一个位置,这可是很耗时的。所以,对于ArrayList,它在集合的末尾删除或添加元素所用的时间是一致的,但是在列表中间的部分添加或删除时所用时间就会大大增加。但是它在根据索引查找元素的时候速度很快。

LinkedList部分说明:LinkedList底层是双向列表
当需要在首位置插入元素时,first 引用指向需要插入到链表中的节点对象,新的节点对象的next引用指向原先的首节点对象;
所以,对于LinkedList,它在插入、删除集合中任何位置的元素所花费的时间都是一样的,但是它根据索引查询一个元素的时候却比较慢。

ArrayList和LinkedList的缺点如下:
对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。
在ArrayList集合中添加或者删除一个元素时,当前的列表移动元素后面所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。
LinkedList集合不支持 高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。
ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
ArrayList和LinkedList的应用场景如下:
ArrayList使用在查询比较多,但是插入和删除比较少的情况,而LinkedList用在查询比较少而插入删除比较多的情况

  • 讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。

父类静态变量、
父类静态代码块、
子类静态变量、
子类静态代码块、
父类非静态变量(父类实例成员变量)、
父类构造函数、
子类非静态变量(子类实例成员变量)、
子类构造函数。

  • 用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。

HashMap、HashTable、LinkedHashMap和TreeMap。

  • HashMap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。
     当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题
      HashMap不支持线程的同步,是非线程安全的,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用 Collections和synchronizedMap方法使HashMap具有同步能力,或者使用ConcurrentHashMap。            
    
  •   Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空。
    它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了 Hashtable在写入时会比较慢。
    
  • LinkedHashMap 保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和容量有关。
    
  • TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
    


一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入、删除和定位元素,HashMap 是最好的选择。
TreeMap取出来的是排序后的键值对。但如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
LinkedHashMap是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

  • JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。

jdk8 放弃了分段锁而是用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。
1、ConcurrentHashMap有哪些构造函数?
一共有五个,作用及代码如下:

//无参构造函数
   public ConcurrentHashMap() {
   }
   //可传初始容器大小的构造函数
   public ConcurrentHashMap(int initialCapacity) {
       if (initialCapacity < 0)
           throw new IllegalArgumentException();
       int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                  MAXIMUM_CAPACITY :
                  tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
       this.sizeCtl = cap;
   }
   //可传入map的构造函数
   public ConcurrentHashMap(Map m) {
       this.sizeCtl = DEFAULT_CAPACITY;
       putAll(m);
   }
   //可设置阈值和初始容量
   public ConcurrentHashMap(int initialCapacity, float loadFactor) {
       this(initialCapacity, loadFactor, 1);
   }
   //可设置初始容量和阈值和并发级别
   public ConcurrentHashMap(int initialCapacity,
                            float loadFactor, int concurrencyLevel) {
       if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
           throw new IllegalArgumentException();
       if (initialCapacity < concurrencyLevel)   // Use at least as many bins
           initialCapacity = concurrencyLevel;   // as estimated threads
       long size = (long)(1.0 + (long)initialCapacity / loadFactor);
       int cap = (size >= (long)MAXIMUM_CAPACITY) ?
           MAXIMUM_CAPACITY : tableSizeFor((int)size);
       this.sizeCtl = cap;
   }

2、ConcurrentHashMap使用什么技术来保证线程安全?
jdk1.7:Segment+HashEntry来进行实现的;
jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized来保证线程安全;
3、ConcurrentHashMap的get方法是否要加锁,为什么?
不需要,get方法采用了unsafe方法,来保证线程安全。
4、ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?
弱一致性,hashmap强一直性。
ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException,因为HashMap包含一个修改计数器,当你调用他的next()方法来获取下一个元素时,迭代器将会用到这个计数器。
5、ConcurrentHashMap1.7和1.8的区别:
jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node
数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized
但是ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock,虽然说jdk8的synchronized的性能进行了优化,但是我觉得还是使用ReentrantLock锁能更多的提高性能

  • 有没有有顺序的Map实现类,如果有,他们是怎么保证有序的?

1、 Map的实现类有HashMap,LinkedHashMap,TreeMap。
2、 HashMap是有无序的,LinkedHashMap和TreeMap都是有序的(LinkedHashMap记录了添加数据的顺序;TreeMap默认是自然升序、key升序)。
3、 LinkedHashMap底层存储结构是哈希表+链表,链表记录了添加数据的顺序。
4、 TreeMap底层存储结构是二叉树,二叉树的中序遍历保证了数据的有序性,基于比较器Comparator来实现有序的。
5、 LinkedHashMap有序性能比较高,因为底层数据存储结构采用的哈希表,基于链表来实现数据插入有序的。

  • 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么?

1、接口可以继承接口,抽象类不可以继承接口,但可以实现接口。
2、抽象类可以继承实体类。抽象类可以实现(implements)接口,抽象类是可继承实体类,但前提是实体类必须有明确的构造函数。
3.抽象类可以继承实体类,就是因为抽象类的可以继承性和有方法。
4、一个接口可以继承多个接口. interface C extends A, B {}是可以的. 一个类可以实现多个接口: class D implements A,B,C{} 但是一个类只能继承一个类,不能继承多个类 class B extends A{} 在继承类的同时,也可以继承接口: class E extends D implements A,B,C{} 这也正是选择用接口而不是抽象类的原因。

  • 继承和聚合的区别在哪?

继承:他是is-a的关系,指一个类继承另外一个类的功能
  例如:public class A extends B { }
聚合:他是has-a  
  例如:public class A{ List b} A可以有b
组合:他是contans-a(强聚合)
  public class A { B b} A一定有b

  • IO模型有哪些,讲讲你理解的nio ,他和bio,aio的区别是啥,谈谈reactor模型。

JAVA BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程并处理,如果这个连接不做任何事情会造成不必要的开销,当然可以通过线程池机制改善
JAVA NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理
JAVA AIO(NIO2):异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理

  • 反射的原理,反射创建类实例的三种方式是什么?

什么是Java反射机制
当程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。我们认为java并不是动态语言,但是它却有一个非常突出的动态相关机制,俗称:反射。
IT行业里这么说,没有反射也就没有框架,现有的框架都是以反射为基础。在实际项目开发中,用的最多的是框架,填的最多的是类,反射这一概念就是将框架和类揉在一起的调和剂。所以,反射才是接触项目开发的敲门砖!
二、反射的应用及原理
我们可能听过,Java编写的程序,一次编译,到处运行。这也是Java程序为什么是无关平台的所在,原因在于,java的源代码会被编译成.class文件字节码,只要装有Java虚拟机JVM的地方(Java提供了各种不同平台上的虚拟机制,第一步由Java IDE进行源代码编译,得到相应类的字节码.class文件,第二步,Java字节码由JVM执行解释给目标计算机,第三步,目标计算机将结果呈现给我们计算机用户;因此,Java并不是编译机制,而是解释机制),.class文件畅通无阻。
Java的反射机制,操作的就是这个.class文件,首先加载相应类的字节码(运行eclipse的时候,.class文件的字节码会加载到内存中),随后解剖(反射 reflect)出字节码中的构造函数、方法以及变量(字段),或者说是取出,我们先来定义一个类Animal,里面定义一些构造函数,方法,以及变量:
所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底的了解自身的情况为下一步的动作做准备。下面具体介绍一下java的反射机制。这里你将颠覆原来对java的理解。 Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的是类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组 成部分。
Java反射之类的实例对象的三种表示方式:

public class ClassDemo1 {
public static void main(String[] args) {
    
    //Foo的实例对象如何表示
    Foo foo1 = new Foo();//foo1就表示出来了
    //Foo这个类,也是一个实例对象,Class类的实例对象,如何表示呢、
    //任何一个类都是Class的实例对象,这个实例对象那个有三个表示方式
    //第一种表示方式--》实际在告诉我们任何一个类都有一个隐含的静态成员变量class
    Class class1 = Foo.class;
    
    //第二种表示方式  已经知道该类的对象通过getClass方法
    Class class2 = foo1.getClass();
    
    /*
     * 官网class1 ,class2表示了Foo类的类类型(class type)
     * 万事万物 都是对象
     * 类也是对象,是Class类的实例对象
     * 这个对象我们称为该类的类类型
     */
    //不管class1  or class2都代表了Foo类的类类型,一个类只可能是Class;类的一个实例对象
    System.out.println(class1==class2);//true'
    
    //第三种表达方式
    Class class3 = null;
    try {
        class3 = Class.forName("com.imooc.reflect.Foo");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    //
    System.out.println(class2==class3);//true
    
    //我们完全尅通过类的类类型创建该类的对象实例--》通过class1  or class2 or class3
    //创建Foo类的实例对象
    try {
        //需要有无参数的构造方法
        Foo foo = (Foo) class1.newInstance();//需要强转
        foo.print();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
}
//
class Foo{
    public void print(){
        System.out.println("foo");
    }
}
  • 反射中,Class.forName和ClassLoader区别 ?

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表示不进行链接,由上面介绍可以,
不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

  • 描述动态代理的几种实现方式,分别说出相应的优缺点。

一、原理区别:
jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。

总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

如何强制使用CGLIB实现AOP?
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入

JDK动态代理和CGLIB字节码生成的区别?
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因为是继承,所以该类或方法最好不要声明成final 。

  • final的用途?

1、被final修饰的类不可以被继承
2、被final修饰的方法不可以被重写
3、被final修饰的变量不可以被改变;被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的。
a、被final修饰的方法,JVM会尝试为之寻求内联,这对于提升Java的效率是非常重要的。因此,假如能确定方法不会被继承,那么尽量将方法定义为final的。
b、被final修饰的常量,在编译阶段会存入调用类的常量池中。

  • 写出三种单例模式实现 。
  1、饿汉式
/**
* 单例模式:饿汉式
* 在类加载时,就创建单例对象
* 执行效率高,但是占空间,以空间换时间
* 线程安全
*/
public class Hungry {
   private static final Hungry hungry = new Hungry();
   private Hungry(){}
   public static Hungry getInstance(){
       return  hungry;
   }
}
 2、懒汉式

第一种:双重判断

/**
* 单例模式:懒汉式  双重判断
* 对象使用的时候,才去创建
* 有线程安全的风险,需要加锁
*/
public class Lazy1 {
   private Lazy1(){}
   private static Lazy1 instance = null;
   public static Lazy1 getInstance(){
       if(instance == null){
           synchronized (Lazy1.class){
               if(instance == null){
                   instance = new Lazy1();
               }
           }
       }
       return instance;
   }
}

第二种:静态内部类

/**
*单例模式:懒汉式  静态内部类
* 静态内部类在使用时,才加载
* 此种模式,既是懒加载,又没有加锁影响性能
*/
public class Lazy2 {
   private Lazy2(){}
   public static final Lazy2 getInstance(){
       return LazyLoad.instance;
   }
   private static class LazyLoad{
      private static final Lazy2 instance = new Lazy2();
  }
}

3、注册式

/**
 * 懒汉式的变种
 * spring IOC容器就是使用的这种注册式的单例模式
 */
public class BeanFactory {
    public static Map ioc = new ConcurrentHashMap();
    public static Object getBean(String className){
       if(!ioc.containsKey(className)){
            try {
                Object instance = Class.forName(className).newInstance();
               ioc.put(className, instance);
               return instance;
           } catch (Exception e) {
               e.printStackTrace();
           }
       }
       return ioc.get(className);
   }
}
  • 如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣?

覆盖equals时需要遵守的通用约定
覆盖equals方法看起来似乎很简单,但是如果覆盖不当会导致错误,并且后果相当严重。《Effective Java》一书中提到“最容易避免这类问题的办法就是不覆盖equals方法”,这句话貌似很搞笑,其实想想也不无道理,其实在这种情况下,类的每个实例都只与它自身相等。如果满足了以下任何一个条件,这就正是所期望的结果:

  • 类的每个实例本质上都是唯一的。对于代表活动实体而不是值的类来说却是如此,例如Thread。Object提供的equals实现对于这些类来说正是正确的行为。
  • 不关心类是否提供了“逻辑相等”的测试功能。假如Random覆盖了equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了。
  • 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。
  • 类是私有的或者是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防止它被意外调用:

在覆盖equals方法的时候,你必须要遵守它的通用约定。下面是约定的内容,来自Object的规范[JavaSE6]

  • 自反性。对于任何非null的引用值x,x.equals(x)必须返回true。
  • 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
  • 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用该x.equals(y)就会一直地返回true,或者一致地返回false。
  • 对于任何非null的引用值x,x.equals(null)必须返回false。

结合以上要求,得出了以下实现高质量equals方法的诀窍:
1.使用==符号检查“参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
2.使用instanceof操作符检查“参数是否为正确的类型”。如果不是,则返回false。一般来说,所谓“正确的类型”是指equals方法所在的那个类。
3.把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
4.对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true;否则返回false。
5.当编写完成了equals方法之后,检查“对称性”、“传递性”、“一致性”。
注意:

  • 覆盖equals时总要覆盖hashCode 《Effective Java》作者说的
  • 不要企图让equals方法过于职能。
  • 不要将equals声明中的Object对象替换为其他的类型(因为这样我们并没有覆盖Object中的equals方法哦)

覆盖equals时总要覆盖hashCode
一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
  • 如果两个对象根据equals()方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
  • 如果两个对象根据equals()方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生相同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
  • 请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用?

访问修饰符,主要标示修饰块的作用域,方便隔离防护。
public: Java语言中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包(package)访问。
private: Java语言中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的类、属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问。
protect: 介于public 和 private 之间的一种访问修饰符,一般称之为“保护形”。被其修饰的类、属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
default: 即不加任何访问修饰符,通常称为“默认访问模式“。该模式下,只允许在同一个包中进行访问。

类的只有两种public,default(不同包不可以访问)
public–都可访问(公有)
private–类内可访问(私有)
protected–包内和子类可访问(保护)
不写(default)–包内可访问 (默认)

public>protected>default>private
Java 方法默认访问级别 : 包访问

Java 类默认访问级别 : 包访问对于一个Class的成员变量或成员函数,如果不用public, protected, private中的任何一个修饰,那么该成员获得“默认访问控制”级别,即package access (包访问)。属于package access的成员可以被同一个包中的其他类访问,但不能被其他包的类访问。

包访问的控制力弱于private,但强于protected。因为一方面,只要是子类,不管子类与父类是否位于同一个包中,那么子类都可以访问父 类中的protected方法。但是一旦位于原类的包外,不管是否是其子类,都无法访问其属于package access级别的成员。而另一方面,一个类可以访问同一个包中另一个类的package access成员,同时也能访问其protected成员。

(注:package是Java中的关键字,虽然包访问也是一种访问控制级别,但关键字”package”只能用来表示类属于哪个包,而不能像”private”,”public”那样放到成员变量或函数前面,作为访问控制修饰符。)

访问级别保护的强度:public

  • 深拷贝和浅拷贝区别?

深复制和浅复制最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。
一篇分析的不错的文章

  • 数组和链表数据结构描述,各自的时间复杂度。

一、各自的特点:
数组:
数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。
链表:
链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。
二、数组和链表的区别:
1、从逻辑结构角度来看:
数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)
2、数组元素在栈区,链表元素在堆区;
3、从内存存储角度来看:
(静态)数组从栈中分配空间, 对于程序员方便快速,但自由度小。
链表从堆中分配空间, 自由度大但申请管理比较麻烦。
数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);
数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。

  • error和exception的区别,CheckedException,RuntimeException的区别?

要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Error:Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

image.png

  • 请列出5个运行时异常。
  • NullPointerException - 空指针引用异常
  • ClassCastException - 类型强制转换异常
  • IndexOutOfBoundsException - 下标越界异常
  • ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
  • BufferOverflowException - 还有IO操作的异常
  • IllegalArgumentException - 传递非法参数异常
  • ArithmeticException - 算术运算异常
  • NegativeArraySizeException - 创建一个大小为负数的数组错误异常
  • NumberFormatException - 数字格式异常
  • SecurityException - 安全异常
  • UnsupportedOperationException - 不支持的操作异常
  • 在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么?

类加载使用的是双亲委派模型,当你想要加载一个类的时候,必须先给你的父加载器,它再去想办法加载,如果它不能加载,再告诉我们,我们自己想办法。
所以,在java中java.lang.String肯定在上层的ClassLoader被加载过了,所以你自己写的完全没有机会加载。
双亲委派模型

  • 说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。

Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的 字段等)映射成一个数值,这个数值称作为散列值。
在Object这个类里面提供的Equals()方法默认的实现是比较当前对象的引用和你要比较的那个引用它们指向的是否是同一个对象。
在设计一个类的时候往往需要重写equals方法,比如String类,在重写equals方法的同时,必须重写hashCode方法。如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致,如在存储散列集合时(如Set类),将会存储了两个值一样的对象,导致混淆,因此,就也需要重写hashcode()。

  • 在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题?

首先我们要了解:什么是向下转型向上转型
面向对象的转型只会发生在具有继承关系的父子类中(接口也是继承的一种)
向上转型:其核心目的在于参数的统一上,根本不需要强制类型转换。
向下转型:是为了操作子类定义的特殊功能,需要强制类型转换,可是现在存在的问题是:向下转型其实是一种非常不安全的操作,以为编译的时候,程序不会报错,而在运行的时候会报错,这就是传说中的—迷之报错。

泛型的核心意义在于:类在进行定义的时候可以使用一个标记,此标记就表示类中属性或者方法以及参数的类型,标记在使用的时候,才会去动态的设置类型。
泛型主要针对向下转型时所带来的安全隐患,其核心组成是在声明类或接口时,不设置参数或属性的类型。

  • 有没有可能2个不相等的对象有相同的hashcode?

1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
2、如果两个对象不equals,他们的hashcode有可能相等。
3、如果两个对象hashcode相等,他们不一定equals。
4、如果两个对象hashcode不相等,他们一定不equals。

  • Java中的HashSet内部是如何工作的?

HashSet实现了Set接口
HashSet依赖的数据结构是哈希表
因为实现的是Set接口,所以不允许有重复的值
插入到HashSet中的对象不保证与插入的顺序保持一致。对象的插入是根据它的hashcode
HashSet中允许有NULL值
HashSet也实现了Searlizable和Cloneable两个接口
所有Set接口的类内部都是由Map做支撑的。HashSet用HashMap对它的内部对象进行排序。你一定好奇输入一个值到HashMap,我们需要的是一个键值对,但是我们传给HashSet的是一个值。
那么HashMap是如何排序的?
实际上我们插入到HashSet中的在map对象中起的是键的作用,因为它的 Java用了一个常量。所以在键值对中所有的键的值都是一样的。

  • 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决?

什么是序列化
Java对象序列化是指将Java对象转化为·字节序列的过程而反序列化则是将字节序列转化为Java对象的过程

为什么需要序列化
我们知道不同线程/进程进行远程通信时可以相互发送各种数据,包括文本图片音视频等,Java对象不能直接传输,所以需要转化为二进制序列传输,所以需要序列化

怎么序列化
实现Serializable接口实现这个接口就会添加一个serialVersionUID的版本号,这个版本号的作用是用来验证反序列化时是否用的是同一个类
序列化是通过ObjectOutputStream类的writeObject()方法将对象直接写出
反序列化时通过ObjectInputStream类的readObject()方法从流中读取数据

反序列化会遇到什么问题
可能会导致InvalidClassException异常
如果没有显式声明序列版本UID,对对象的需求进行了改动,那么兼容性将会遭到破坏,在运行时导致InvalidClassException

  • java8的新特性。

1、 Lambda表达式和函数式接口
2、 方法引用
3、 Stream流
4、 并行API
5、 Time日期时间
6、 接口的默认方法和静态方法
7、 重复注解
8、 更好的类型推断
9、 拓宽注解的应用场景
10、 参数名称
11、 Optional
参考1
参考2

你可能感兴趣的:(JAVA基础)