JAVA面试——JAVA基础

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhengzhaoyang122/article/details/82183482

1、JAVA中的几种基本数据类型是什么,各自占用多少字节。
      Java语言中一共提供了8种原始的数据类型(byte,short,int,long,float,double,char,boolean),这些数据类型不是对象,而是Java语言中不同于类的特殊类型,这些基本类型的数据变量在声明之后就会立刻在栈上被分配内存空间。除了这8种基本的数据类型外,其他类型都是引用类型(例如类、接口、数组等),引用类型类似于C++中的引用或指针的概念,它以特殊的方式指向对象实体,此类变量在声明时不会被分配内存空间,只是存储了一个内存地址而已。

数据类型 字节长度 范围 默认值 包装类
int 4 (-2^31~2^31-1) 0 Integer
short 2 [-32768,32767] 0 Short
byte 1 [-128,127] 0 Byte

long

8 (-2^63~2^63-1) 0L或0l Long
double 8

64位IEEE754双精度范围

0.0 Double
float 4 32位IEEE754单精度范围 0.0F或0.0f Float
char 2 Unicode [0,65535] u0000 Character
boolean 1 true和false flase Boolean

2、String类能被继承吗,为什么。
     不可以,因为String类有final修饰符,而final修饰的类是不能被继承的,实现细节也不允许改变。

3、String,Stringbuffer,StringBuilder的区别。
  ● String是不可变类,String对象一旦被创建,其值就不能改变,而StringBuffer是可变类,当对象被创建后仍然可以对其值进行修改。由于String是不可变类,因此适合在需要被共享的场合中使用,而当一个字符串经常被修改时,最好使用StringBuffer来实现。如果用String保存一个经常修改的字符串时,字符串被修改时会比StringBuffer多很多附加的操作,同时生成很多无用的对象,由于这些无用的对象会被牢记回收器来回收,因此会影响程序的性能。在规模小的项目里面这个影响很小,但是在一个规模大的项目里面,这会对程序的运行效率带来很大的影响。
  ● String与StringBuffer实例化时存在区别:String可以通过构造函数的方式(String s = new String("hello"))和直接赋值(String s="world")两种方式。而StringBuffer只能使用构造函数进行赋值(StringBuffer sb = new StringBuffer("hello"))。
  ● String字符串修改实现的原理:当String修改字符串时,先创建一个StringBuffer,其次调用append()方法,最后调用toString()方法把结果返回。实例如下(下述过程比使用StringBuffer多了一些附加操作,同时也生成了一些临时的对象,从而导致程序执行效率下降):

String s = "HELLO";
s+="WORLD";
//以上代码 实现底层 如下
StringBuffer sb = new StringBuffer(s);
sb.append("WORLD");
s=sb.toString();

   ● StringBuilder也是可以被修改的字符串,他与StringBuffer类似,都是字符缓冲区,但StringBuild不是线程安全的,如果只是单线程访问时可以使用StringBuilder,当有多个线程访问时,最好使用线程安全的StringBuffer。因为StringBuffer必要时会对这些方法进行同步,所以任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
  ● 在执行效率方面,StringBuilder最高,StringBuffer次之,String最低,鉴于以上情况,一般使用数据量较小的情况下,优先使用String;如果单线程下使用大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先考虑StringBuffer类。

4、ArrayList、Vector和LinkedList有什么区别。
    ArrayList、Vecotr、LinkedList类均为java.util包中,均为可伸缩数据,既可以动态改变长度的数组。
☞ ArrayList和Vector都是基于存储的Object[] array来实现的,它们会在内存中开辟一块连续的空间来存储,由于数据村粗时连续的,因此,它们支持用序号(下标)来访问元素,同时索引数据的速度比较快。但是在插入元素时需要移动容器中的元素,所以对数据的插入操作执行比较慢。ArrayList和Vector都是一个初始化的容量的大小,当存储的元素超过分配内存大小时就需要动态地扩展它们的存储空间。为了提高效率,每次扩充容量时,不是简单的扩充一个存储单位,而是一次增加多个存储单元,Vector默认扩充为原来的2倍(每次扩充的大小是可以设置的),而ArrayList默认扩充1.5倍(没有提供方法来设置空间扩充大小)。
☞ ArrayList与Vector最大的区别就是synchronization(同步)的使用,没有一个ArrayList的方法是同步,而Vector的绝大数方法(add、insert、remove、set、equals、hashcode等)都是直接或者间接同步的,所以Vector是线程安全的,ArrayList不是线程安全的。正是由于Vector是线程安全的,所以性能上也略逊于ArrayList。
☞ LinkedList是采用双向列表来实现的,对数据的索引需要从列表头开始遍历,因此用于随机访问效率比较低,但是插入元素时不需要对数据进行移动,因此插入效率高。同时LinkedList是非线程安全的容器。
☞ 那么,在实际使用时,当对数据主要操作为索引或只是集合的未端增加、删除元素时,使用ArralyList或Vector效率比较高;当对数据的操作主要是指位置的插入或者删除操作时,使用LinkedList效率比较高;当在线程中使用容器时(既多线程同时访问该容器),选用Vector较为安全。

5、讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。
    类加载器实例化时进行的操作步骤(加载【查找并加载类的二进制数据】—>连接【1、验证:确保被加载的类的正确性。2、准备:为类的静态变量分配内存,并将其初始化为默认值】—>解析【把类中的符号引用转换为直接引用】 —>初始化【为类的静态变量赋予正确的初始值】)。
    所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。
    执行顺序:父类静态代变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量(父类实例成员变量)、父类构造函数、子类非静态变量(子类实例成员变量)、子类构造函数。

6、用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。
使用过的Map类:1)、HashMap:最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。它是线程不安全的Map,方法上都没有synchronize关键字修饰。HashMap允许空(null)键值,但最多只允许一条记录的键为null。
   2)、HashTable是线程安全的Map实现类,它实现线程安全的方法是在各个方法上添加了synchronize关键字,但是现在已经不再推荐使用HashTable了,因为现在有了ConcurrentHashMap这个专门用于多线程(并发)场景下的map实现类,其大大优化了多线程下的性能。
   3)、如果你经常参加面试,一定会被问到这个map实现类,这个map实现类是在jdk1.5中加入的,其在jdk1.6/1.7中的主要实现原理是segment段锁,它不再使用和HashTable一样的synchronize一样的关键字对整个方法进行枷锁,而是转而利用segment段落锁来对其进行加锁,以保证Map的多线程安全。
   其实可以理解为,一个ConcurrentHashMap是由多个HashTable组成,所以它允许获取到不同段锁的线程同时持有该资源,也就是说segment有多少个,理论上就可以同时有多少个线程来持有它这个资源。其默认的segment是一个数组,默认长度为16。也就是说理论商可以提高16倍的性能。
   在JAVA的jdk1.8中则对ConcurrentHashMap又再次进行了大的修改,取消了segment段锁字段,采用了CAS+Synchronize技术来保障线程安全。具体7中有介绍。
   4)、TreeMap:是一个很常用的map实现类,因为他具有一个很大的特点就是会对Key进行排序,使用了TreeMap存储键值对,再使用iterator进行输出时,会发现其默认采用key由小到大的顺序输出键值对,如果想要按照其他的方式来排序,需要重写也就是override 它的compartor接口。TreeMap底层的存储结构也是一颗红黑树。红黑树查找效率高,只有O(lgn)。它是一种自平衡的二叉查找树。在每次插入和删除节点时,都可以自动调节树结构,以保证树的高度是lgn。

 public class Compare {
     public static void main(String[] args) {
     TreeMap<String,Integer> map = new TreeMap<String,Integer>(new xbComparator());
         map.put("key_1", 1);
         map.put("key_2", 2);
         map.put("key_3", 3);   
         Set<String> keys = map.keySet();
         Iterator<String> iter = keys.iterator();
         while(iter.hasNext()){
             String key = iter.next();
             System.out.println(" "+key+":"+map.get(key));
         }
     }
 }
     //重写排序方法
     class xbComparator implements Comparator{
         public int compare(Object o1,Object o2){
         String i1=(String)o1;
         String i2=(String)o2;
         return -i1.compareTo(i2);
     }
 }

    5)、LinkedHashMap:它的特点主要在于linked,带有这个字眼的就表示底层用的是链表来进行的存储。相对于其他的无序的map实现类,还有像TreeMap这样的排序类,linkedHashMap最大的特点在于有序,但是它的有序主要体现在先进先出FIFIO上。没错,LinkedHashMap主要依靠双向链表和hash表来实现的。
   6)、WeakHashMap:与HashMap类似,二者不同之处在于WeakHashMap中的key不再被外部引用,它就可以被垃圾回收器回收。而HashMap中key采用的是“强引用的方式”,当HashMap中的key没有被外部引用时,只有在这个key从HashMap中删除后,才可以被垃圾回收器回收。

7、Java8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
  
ConcurrentHashMap分段锁中存在一个分段锁个数的问题,既Segment[]的数组长度。当长度设置小了,数据结构根据额外的竞争,从而导致线程线程视图写入当前锁定的段,导致阻塞。相反,如果高估了并发级别,当遇到过大的膨胀(大量的并发),由于段产生的不必要数量,这种膨胀会导致性能的下降。因为高速缓存未命中。而Java8中仅仅是为了兼容旧版本而保留。唯一的作用就是保证构造map时初始容量不小于concurrencyLevel。
   在Java的jdk1.8中则对ConcurrentHashMap采用CAS+Synchronize技术来保障线程安全,底层采用数组+链表+红黑树的存储结构,也就是和HashMap一样。这里注意Node其实就是保存一个键值对的最基本对象。其中value和next都是使用的volatile关键字进行了修饰,以确保线程安全。
   volatile修改变量后,此变量就具有可见性,一旦该变量修改,其他线程立马就会知道,立马放弃自己在自己工作内存中持有的该变量值。转而重主内存中获取该变量最新的值。
   在插入元素时,会首先进行CAS判定,如果OK就是插入其中,并将size+1,但是如果失败了,就会通过自旋锁自旋后再次尝试插入,直到成功。
   所谓CAS也就是Compare And Swap,既在更改前先对内存中的变量值和你指定的那个变量值进行比较,如果相同就说明再次期间没有被修改,而如果不一样了,则就要停止修改,否则就会影响到其他人的修改,将其覆盖掉。举例:内存值a,旧值b,和要修改后的值c,如果这里a=b,那么就可以进行更改,就可以将内存值a=c。否则就要终止该更新操作。如果链表中存储的Entry超过了8个则就会自动转换链表为红黑树,提高查询效率。

8、有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。
   TreeMap和LinkedHashMap是有序的(TreeMap默认升序,LinkedHashMap则记录了插入顺序)。
● ThreeMap是如何保证其迭代输出是有序的呢?其实从宏观上来讲,就相当于树的中序遍历(LDR)。
● LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。

9、抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。
  
1)、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
   2)、抽象类要被子类继承,接口要被类实现。
   3)、接口只能做方法声明,抽象类中可以做方法声明,也可以做方法实现。
   4)、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
   5)、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
   6)、抽象方法只能声明,不能实现。abstract void abc();不能写成abstract void abc(){}。
   7)、抽象类里可以没有抽象方法,但如果一个类里有抽象方法,那么这个类只能是抽象类。
   8)、抽象方法要被实现,所以不能是静态的,也不能是私有的。
   9)、接口可继承接口,并可多继承接口,但类只能单根继承。

10、继承和聚合的区别在哪。
● 继承:指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;在Java中此类关系通过关键字extends明确标识,在设计时一般没有争议性;
● 聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;

11、IO模型有哪些,讲讲你理解的NIO ,他和BIO,AIO的区别是啥,谈谈Reactor模型。
 
之前写过一篇NIO与IO:https://blog.csdn.net/zhengzhaoyang122/article/details/81410115
 
Reactor:前提:一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。事件分发器:即将那些读写事件源分发给各读写事件的处理者。开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
   涉及到事件分发器的两种模式称为:Reactor(NIO通常采用Reactor模式)和Proactor(AIO通常采用Proactor模式)。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
   在Reactor中实现读:1)、注册读就绪事件和相应的事件处理器。2)、事件分离器等待事件。3)、事件到来,激活分离器,分离器调用事件对应的处理器。4)、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

12、反射的原理,反射创建类实例的三种方式是什么。
 ☞ 反射原理:Java语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。
  1)、获取类对象:通过类名获取Class对象,Class<T> c = Class.forName("类的完全路径");通过Class对象获取具体的类对象:Object o = (Object) c.newInstance();
  2)、获取类中的构造方法:getConstructor()等方法;
  3)、获取类中的属性:getFields()等方法;
  4)、获取类中的方法:getMethods()等方法;
 ☞ 创建类实例的三种方式 :1)、调用类的Class对象的newInstance方法,该方法会调用对象的默认构造器,如果没有默认构造器,会调用失败。

Class<?> classType = ExtendType.class;
Object inst = classType.newInstance();
System.out.println(inst);

//输出:
/*Type:Default Constructor
 *ExtendType:Default Constructor
 *com.quincy.ExtendType@d80be3
 */

  2)、调用默认Constructor对象的newInstance方法:

Class<?> classType = ExtendType.class;
Constructor<?> constructor1 = classType.getConstructor();
Object inst = constructor1.newInstance();
System.out.println(inst);

//输出:
/*Type:Default Constructor
 *ExtendType:Default Constructor
 *com.quincy.ExtendType@1006d75
 */

  3)、调用带参数Constructor对象的newInstance方法:

Constructor<?> constructor2 =classType.getDeclaredConstructor(int.class, String.class);
Object inst = constructor2.newInstance(1, "123");
System.out.println(inst);

//输出:
/*Type:Default Constructor
 *ExtendType:Constructor with parameters
 *com.quincy.ExtendType@15e83f9
 */

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

14、描述动态代理的几种实现方式,分别说出相应的优缺点。
  
1)、JDK动态代理:底层封装了实现细节,格式固定,代码简单。直接调用java.lang.reflect.Proxy静态方法newProxyInstance即可;JDK底层是利用反射机制,需要基于接口方式,这是由于被代理的对象必须是一个类,且必须有父接口;被代理的类需要增强的方法必须在父接口中出现;

/* 1、ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
 * 2、Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
 * 3、InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
 */
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )

   2)、CGlib动态代理也叫作子类代理:则是基于ASM框架,实现了无反射机制进行代理,利用空间来换取了时间,代理效率高于JDK。它是在内存中构建一个子类对象从而实现对目标对象功能的扩展,广泛的被许多AOP的框架使用。
举个栗子:被代理的类:

public class TargetProxy {
	public void save() {
		System.out.println("假如这个是Spring的service层的方法");
	}
}

   代理对象:

public class ProxyFactory implements MethodInterceptor {
	private Object target;
 
	public ProxyFactory(Object target) {
		this.target = target;
	}
 
	// 给目标对象创建一个代理对象
	public Object getProxyInstance() {
		Enhancer en = new Enhancer();
		en.setSuperclass(target.getClass());
                // 回调方法
		en.setCallback(this);
                // 创建代理对象
		return en.create();
	}
 
	@Override
       /** 
        * 调用方法 
        */  
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		System.out.println("开始事务...");
                //执行方法
		Object returnValue = method.invoke(target, args);
		System.out.println("提交事务...");
		return returnValue;
	}
}

  执行测试 :

public class CglibProxy {
	public static void main(String[] args) {
		// 目标对象
		TargetProxy target = new TargetProxy();
		ProxyFactory factory = new ProxyFactory(target);
		target = (TargetProxy) factory.getProxyInstance();
		// 执行代理对象的方法
		target.save();
	}
 
}

15、动态代理与CGlib实现的区别。
   
动态代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理。但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫Cglib代理。

16、为什么CGlib方式可以对接口实现代理。
  
因为Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口。

17、final的用途。
  
final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
   ● final变量:凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。
   ● final方法:方法前面加上final关键字,代表这个方法不可以被子类的方法重写。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。
   ● final类:final修饰的类通常功能是完整的,它不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。

18、写出三种单例模式实现 。
  
1)、懒汉式单例模式,线程不安全。

//这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static synchronized Singleton getInstance() {
	if (instance == null) {
	    instance = new Singleton();
	}
	return instance;
    }
}

   2)、饿汉式单例模式

/*
 *这种方式基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化。目前java单例是指一
 *个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的ClassLoader装载饿汉式实现单
 *例类的时候就会创建一个类的实例。这就意味着一个虚拟机里面有很多ClassLoader,而这些classloader都能
 *装载某个类的话,就算这个类是单例,也能产生很多实例。当然如果一台机器上有很多虚拟机,那么每个虚拟机
 *中都有至少一个这个类的实例的话,那这样 就更不会是单例了。(这里讨论的单例不适合集群!) 
 */
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

   3)、静态内部类

/*这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,这种方式是Singleton类被
 *装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,
 *才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载!这个时候,这种方式相比第2种方式就显得很合理。
 */
public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
} 

   4)、枚举方式

/*这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反
 *序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写
 *不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
 */
public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

   5)、双重校验锁(jdk1.5)

/*这样方式实现线程安全地创建实例,而又不会对性能造成太大影响。它只是第一次创建实例的时候同步,以后就不需要同步了。
 *由于volatile关键字屏蔽了虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此建议没有特别的需要不要使用。双重检验锁方式的单例不建议大量使用,根据情况决定。 
 */
public class Singleton {
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
	if (singleton == null) {
	    synchronized (Singleton.class) {
		if (singleton == null) {
		    singleton = new Singleton();
		}
	    }
	}
	return singleton;
    }
}

19、如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣。
  
父类已经覆盖了equals,从父类继承过来的行为对于子类也是合适的。大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

20、请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。

  同一个类 同一个包 不同包的子类 不同包的非子类
Private      
Default    
Protected  
Public

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

21、深拷贝和浅拷贝区别。
  ● 浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。
  ● 深拷贝(.clone())不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

22、数组和链表数据结构描述,各自的时间复杂度。
  ● 数组:是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少插入和删除元素,就应该用数组。数组从栈中分配空间, 对于程序员方便快速,但自由度小。数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。
  ● 链表:中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表。链表从堆中分配空间, 自由度大但申请管理比较麻烦。链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。
  ● 时间复杂度:按序号查找时,数组可以随机访问,时间复杂度为O(1)。链表不支持随机访问,平均需要O(n)。按值查找时,若数组无序,数组和链表时间复杂度均为O(1),但是当数组有序时,可以采用折半查找将时间复杂度降为O(logn);插入和删除时,数组平均需要移动n/2个元素,而链表只需修改指针即可;

23、Error和Exception的区别,CheckedException,RuntimeException的区别。
      
  ♣ Error:当程序发生不可控的错误时,通常做法是通知用户并中止程序的执行。与异常不同的是Error及其子类的对象不应被抛出。Error是throwable的子类,代表编译时间和系统错误,用于指示合理的应用程序不应该试图捕获的严重问题。Error由Java虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。
  ♣ Exception:一般分为Checked异常和Runtime异常,所有RuntimeException类及其子类的实例被称为Runtime异常,不属于该范畴的异常则被称为CheckedException。
  1)、Checked异常:只有java语言提供了Checked异常,Java认为Checked异常都是可以被处理的异常,所以Java程序必须显示处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误无法编译。这体现了Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked异常处理方法有两种:①、当前方法知道如何处理该异常,则用try-catch块来处理该异常。②、当前方法不知道如何处理,则在定义该方法是声明抛出该异常。我们比较熟悉的Checked异常有:
   √ Java.lang.ClassNotFoundException
   √ Java.lang.NoSuchMetodException
   √ Java.io.IOException
   2)、RuntimeException:Runtime如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。我们比较熟悉的RumtimeException类的子类有:
   √ Java.lang.ArithmeticException
   √ Java.lang.ArrayStoreExcetpion
   √ Java.lang.ClassCastException
   √ Java.lang.IndexOutOfBoundsException
   √ Java.lang.NullPointerException

24、请列出5个运行时异常。
  
● ClassCastException (类转换异常)
   ● IllegalArgumentException (非法参数异常)
   ● IndexOutOfBoundsException (下标越界异常)
   ● NullPointerException (空指针异常)
   ● ArithmeticException  (算术运算异常)
   ● OutOfMemoryError  (内存不足)
   ● StackOverflowError  (堆栈溢出)
   ● ClassNotFoundException  (找不到类异常)
   ● InterruptedException (终止异常)

25、在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么。
  
不会被加载。原因:类加载器的委托机制。
   类的加载过程采用父亲委托机制。这种机制能更好的保证java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader加载Sample类时,loader首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader本身加载Sample类。

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是说当发现这个类没有的时候会先去让自己的父类去加载。那么例子中我们自己写的String应该是被Bootstrap ClassLoader加载了,所以App ClassLoader就不会再去加载我们写的String类了,导致我们写的String类是没有被加载的。 

26、说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。
  
● hashCode:hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值,也就是哈希码,哈希码并不是完全唯一的,它是一种算法,让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。
   ● equals:在Object这个类里面提供的Equals()方法默认的实现是比较当前对象的引用和你要比较的那个引用它们指向的是否是同一个对象。
  ☛ 当我们需要对值进行比较时,就需要重写这两个方法。例如String就重写了这两个方法,比较的是value值是否相等。

27、在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。
 
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,能够解决代码复用的问题。常见的一种情况是,你有一个函数,它带有一个参数,参数类型是A,然而当参数类型改变成B的时候,你不得不复制这个函数。除此之外泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,消除显示的类型强制转换,以提高代码的重用率。
使用方法:

public class Stack<T>{
        private T[] m_item;
        public T Pop(){...}
        public void Push(T item){...}
        public Stack(int i)
        {
            this.m_item = new T[i];
        }
}

  类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类
Stack<int> a = new Stack<int>(100);
      a.Push(10);
      a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据
      int x = a.Pop();

//实例化只能保存string类型的类
Stack<string> b = new Stack<string>(100);
      b.Push(10);    //这一行编译不通过,因为类b只接收string类型的数据
      b.Push("8888");
      string y = b.Pop();

28、这样的a.hashcode() 有什么用,与a.equals(b)有什么关系。
   
hashcode()方法提供了对象的hashCode值,是一个native方法,返回的默认值与System.identityHashCode(obj)一致。通常这个值是对象头部的一部分二进制位组成的数字,具有一定的标识对象的意义存在,但绝不定于地址。
    作用是:用一个数字来标识对象。比如在HashMap、HashSet等类似的集合类中,如果用某个对象本身作为Key,即要基于这个对象实现Hash的写入和查找,那么对象本身如何实现这个呢?就是基于hashcode这样一个数字来完成的,只有数字才能完成计算和对比操作。
    equals与hashcode的关系:equals相等的两个对象,则重写后的hashcode一定要相等。但是hashcode相等的两个对象equals不一定相等。

29、有没有可能2个不相等的对象有相同的hashcode。
  
hashCode是所有java对象的固有方法,如果不重载的话,返回的实际上是该对象在jvm的堆上的内存地址,而不同对象的内存地址肯定不同,所以这个hashCode也就肯定不同了。如果重载了的话,由于采用的算法的问题,有可能导致两个不同对象的hashCode相同。
   hashcode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。接下来有两个个关于这两个方法的重要规范(我只是抽取了最重要的两个,其实不止两个):
  1)、若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的hashcode应该 相等”。不过请注意:这个只是规范,如果你非要写一个类让equals(Object obj)返回true而hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。
   2)、如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的hashcode可能相同”。

30、Java中的HashSet内部是如何工作的。
  
它是基于 HashMap 实现的,底层采用 HashMap 来保存元素。所有Set接口的类内部都是由Map做支撑的。HashSet用HashMap对它的内部对象进行排序。你一定好奇输入一个值到HashMap,我们需要的是一个键值对,但是我们传给HashSet的是一个值。实际上我们插入到HashSet中的值在map对象中起的是键的作用,因为它的值Java用了一个常量。所以在键值对中所有的键的值都是一样的。

class Test
{
    public static void main(String[]args)
    {
        HashSet<String> h = new HashSet<String>();
 
        // adding into HashSet
        h.add("India");
        h.add("Australia");
        h.add("South Africa");
        h.add("India");// adding duplicate elements
 
        // printing HashSet
        System.out.println(h);//[Australia, South Africa, India]
        System.out.println("List contains India or not:" +
                           h.contains("India"));//true
 
        // Removing an item
        h.remove("Australia");
        System.out.println("List after removing Australia:"+h);//[South Africa, India]
 
        // Iterating over hash set items
        System.out.println("Iterating over list:");
        Iterator<String> i = h.iterator();
        while (i.hasNext())
            System.out.println(i.next());//South Africa  India
    }
}

31、什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。
  
序列化 (Serialization):将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
   序列化实现:1)、Serializable接口:类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。 2)、Externalizable接口:除了Serializable 之外,java中还提供了另一个序列化接口Externalizable。Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。3)、ObjectOutput和ObjectInput 接口。4)、ObjectOutputStream类和ObjectInputStream类
   注意:1)、Transient关键字:作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。2)、序列化ID:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。
   为什么序列化:1)、当你想把的内存中的对象保存到一个文件中或者数据库中时候;2)、当你想用套接字在网络上传送对象的时候; 3)、当你想通过RMI传输对象的时候;
   反序列化问题及解决办法:1)、类里面一定要serialVersionUID,否则旧数据会反序列化会失败。
  解决方法:serialVersionUID是根据该类名、方法名等数据生产的一个整数,用来验证版本是否一致。如果不加这个字段,当你的类修改了字段,在反序列化的时候会直接报异常:InvalidCastException,导致无法完成反序列化。
  2)、一旦序列化保存到磁盘操作后,就不要修改类名了,否则旧数据会反序列化会失败。
  解决方法:所以尽量把对象转换成JSON保存更稳妥。

32、java8的新特性。
  
1)、Lambda 表达式:将函数式编程引入了Java。Lambda允许把函数作为一个方法的参数,或者把代码看成数据。

String[] atp = {"Nadal", "Djokovic",  "Wawrinka"};  
List<String> players =  Arrays.asList(atp);  
  
// 以前的循环方式  
for (String player : players) {  
     System.out.print(player + "; ");  
}  
// 使用 lambda 表达式以及函数操作(functional operation)  
players.forEach((player) -> System.out.print(player + "; "));  

   2)、接口的默认方法与静态方法:我们可以在接口中定义默认方法,使用default关键字,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类提供的自己的实现。

public interface DefaultFunctionInterface {
    default String defaultFunction() {
        return "default function";
    }
}

//我们还可以在接口中定义静态方法,使用static关键字,也可以提供实现。
public interface StaticFunctionInterface {
    static String staticFunction() {
        return "static function";
    }
}

   3)、方法引用:通常与Lambda表达式联合使用,可以直接引用已有Java类或对象的方法。
   4)、重复注解:在Java 5中使用注解有一个限制,即相同的注解在同一位置只能声明一次。Java 8引入重复注解,这样相同的注解在同一地方也可以声明多次。重复注解机制本身需要用@Repeatable注解。Java 8在编译器层做了优化,相同注解会以集合的方式保存,因此底层的原理并没有变化。
   5)、扩展注解的支持:Java 8扩展了注解的上下文,几乎可以为任何东西添加注解,包括局部变量、泛型类、父类与接口的实现,连方法的异常也能添加注解。
   6)、Optional:Java 8引入Optional类来防止空指针异常,Optional类最先是由Google的Guava项目引入的。Optional类实际上是个容器:它可以保存类型T的值,或者保存null。使用Optional类我们就不用显式进行空指针检查了。

        Optional<String> str = Optional.of("test");
        //1、判断str是否为null,如果不为null,则为true,否则为false
        if (str.isPresent()) {
            //get用于获取变量的值,当变量不存在的时候会抛出NoSuchElementException,如果不能确定变量一定存在值,则不推荐使用
            str.get();
        }
!

   7)、Stream:Stream API是把真正的函数式编程风格引入到Java中。其实简单来说可以把Stream理解为MapReduce,当然Google的MapReduce的灵感也是来自函数式编程。她其实是一连串支持连续、并行聚集操作的元素。从语法上看,也很像linux的管道、或者链式编程,代码写起来简洁明了,非常酷帅!
   8)、Date/Time API (JSR 310):Java 8新的Date-Time API (JSR 310)受Joda-Time的影响,提供了新的java.time包,可以用来替代 java.util.Date和java.util.Calendar。一般会用到Clock、LocaleDate、LocalTime、LocaleDateTime、ZonedDateTime、Duration这些类,对于时间日期的改进还是非常不错的。
   9)、JavaScript引擎Nashorn:Nashorn允许在JVM上开发运行JavaScript应用,允许Java与JavaScript相互调用。
   10)、Base64:在Java 8中,Base64编码成为了Java类库的标准。Base64类同时还提供了对URL、MIME友好的编码器与解码器。

猜你喜欢

转载自blog.csdn.net/zhengzhaoyang122/article/details/82183482