面试(二)

4.介绍一下java的数据结构,然后手写一个栈的类

 

主要可以分为两类:

1)Java中定义了一个接口collection,用来存储一个元素集合

2)另一种是定义了映射(map)用来存储键/值对。

Collection接口为线性表(list)、向量(vector)、栈(stack)、队列(queue)、优先队列(priority queue)以及规则集(set)定义了通用的操作

  • Set(规则集)用于存储一组不重复的元素。 重要的实现类:HashSet
  • List(线性表)用于存储一个有序元素的集合(允许重复)。两个重要的实现类:ArrayList(数组线性表类)和 LinkedList(链表类)。
  • Stack(栈)用于存储采用后进先出方式处理的对象。
  • Queue(队列)用于采用先进先出方式处理的对象。不过队列用双向链表LinkedList实现更好
  • PriorityQueue用于存储按照优先级顺序处理的对象。

map(映射)是一个存储“键/值对”集合的容器对象。键很像索引,在List中,索引是整数;在Map中,键可以是任意类型的对象。映射中不能有重复的键,每个键都对于一个值。

 

线性表、栈、队列、优先队列:

ArrayList、LinkedList 都是线程不安全的vector是线程安全的。

 

ArrayList:用数组存储元素。这个数组是动态创建的,如果元素个数超过数组容量,就会创建一个更大的新数组,并将当前数组中的所有元素都复制到新数组中。

  • ArrayList的默认容量大小是10。当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”
  • ArrayList实现java.io.Serializable的方式:当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

  • ArrayList中的操作不是线程安全的。
  • ArrayList的克隆函数clone(),即是将全部元素克隆到一个数组中。

LinkedList:LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

  • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  • LinkedList中的操作不是线程安全的。
  • LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。

vector向量类,是AbstractList的子类。vector除了包含用于访问和修改向量的同步方法外,与ArrayList是一样的。

  • vector是线程安全的。对许多不需要同步的应用程序来说,使用ArrayList比vector效率更高。

stack:栈类,继承自vector,提供了后进先出的数据结构。

  • push(o:E)、pop()、peek()

Queue:队列,但一般使用双向链表LinkedList进行队列操作,因为它可以高效的在列表两端插入和删除元素。(LinkedList实现了双端队列Deque接口,Deque又继承自Queue接口)

  • offer(o:E)、poll()、peek()           (poll()和remove()都获取并移除队列头元素,但如果队列为空poll会返回null,而remove会抛出异常)

PriorityQueue:优先队列,默认情况下,使用Comparable以元素的自然顺序进行排序。

  • 拥有最小数值的元素被赋予最高优先级,因此最先从队列中删除。如果几个元素具有相同优先级,则任意选一个。
  • 也可使用构造方法 PriorityQueue( initialCapacity, comparator ) 中的 comparator 来指定一个顺序。

规则集和映射:

规则集:HashSet、LinkedHashSet、TreeSet  (HashSet与TreeSet都是基于Set接口的实现类。其中TreeSet是Set的子接口SortedSet的实现类)

  • HashSet(包括LinkedHashSet)、TreeSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一条线程修改了该Set集合,则必须手动保证该Set集合的同步性。

   通常可以通过Collections工具类的synchronizedSet方法来"包装"该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。

      例如:Set hs = Collections.synchronizedSet(new HashSet());

 

HashSet:实现了Set接口的具体类。默认初始容量16,负载系数0.75。当元素个数超过了容量与负载系数的乘积,容量就会自动翻倍。

  • 存储不重复元素,其中的元素没有顺序
  • 集合元素可以是null,但只能放入一个null。

LinkedHashSet:用一个链表实现来扩展HashSet。支持规则集内的元素顺序。

  • 存储不重复元素,并按它们插入的顺序获取。

TreeSet:实现了SortedSet接口,SortedSet是Set的一个子接口。

  • TreeSet并不是根据元素的插入顺序进行排序,而是根据元素实际值来进行排序的,支持两种排序方式:自然排序 和定制排序,其中自然排序为默认的排序方式。
  • 如果试图把一个对象添加进TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出ClassCastException异常。

如果需要实现定制排序(我们这实现倒序),则需要在创建TreeSet集合对象时,并提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑:

class Person{
    Integer age;
    public Person(int age){
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person [age=" + age + "]";
    }
}
public class Test {
    public static void main(String[] args){
        //实现定制顺序(倒序排)
        TreeSet<Person> persons = new TreeSet<Person>(new Comparator<Person>(){
            @Override
            public int compare(Person o1, Person o2) {
                if(o1.age > o2.age){
                    return -1;
                }else if(o1.age == o2.age){
                    return 0;
                }else{
                    return 1;
                }
            }
        });
        
        persons.add(new Person(2));
        persons.add(new Person(5));
        persons.add(new Person(6));
        
        System.out.println(persons);
    }
}

//打印结果为[Person [age=6], Person [age=5], Person [age=2]]

 

映射:HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap

  • 如果更新映射时不需要保持映射中元素的顺序,用HashMap;
  • 如果需要保持映射中元素的插入顺序或访问顺序,用LinkedHashMap
  • 如果需要使用映射按照键排序,用TreeMap

HashMap、LinkedHashMap、TreeMap都是线程不安全的,ConcurrentHashMap是线程安全的

 

HashMap                                                                                         

HashMap的主体是一个数组,数组中的每个元素是一个单向链表,链表的一个节点是嵌套类Entry的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

  • capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。默认的初始容量为16。
  • loadFactor:负载因子,默认为 0.75。
  • threshold:扩容的阈值,等于 capacity * loadFactor。当HashMap的大小>=阈值,并且新值要插入的数组位置已经有元素了,则进行扩容。

Put方法:

  HashMap会对null值key进行特殊处理,总是放到table[0]位置。

  put过程是先计算key的hash然后通过hash与table.length取模计算index值,然后将键值对放到table[index]位置,当table[index]已存在其它元素时,会在table[index]位置形成一个单向链表,将新添加的元素放在table[index]所对应链表的头部,原来的元素通过Entry的next进行链接,这样以链表形式解决hash冲突问题,当元素数量达到临界值(capactiy*factor)时,则进行扩容,是table数组长度变为table.length*2

get方法:

  同样当key为null时会进行特殊处理,在table[0]的链表上查找key为null的元素。

  get的过程是先计算key的hash然后通过hash与table.length取摸计算index值,然后遍历table[index]上的链表,直到找到目标值,然后返回。

resize方法:

  这个方法实现了非常重要的hashmap扩容,具体过程为:先创建一个容量为table.length*2的新数组,修改临界值,然后把table里面元素计算hash值并使用hash与table.length*2重新计算index放入到新的table里面。

  这里需要注意下是用每个元素的hash全部重新计算index,而不是简单的把原table对应index位置元素简单的移动到新table对应位置。

clear方法:

  遍历table然后把每个位置置为null,同时修改元素个数为0。

  需要注意的是clear方法只会清除里面的元素,并不会重置capactiy。

containsKey和containsValue:

  containsKey方法是先计算hash然后使用hash和table.length取模得到index值,遍历table[index]元素查找是否包含key相同的值。

  containsValue方法就比较粗暴了,就是直接遍历所有元素直到找到value

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

 

 

 ConcurrentHashMap                                                           

HashMap在并发环境下使用中最为典型的一个问题,就是在HashMap进行扩容重哈希时导致Entry链形成环。一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环。

 

ConcurrentHashMap允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术,它使用了不同的锁来控制对哈希表的不同部分进行的修改(写),而 ConcurrentHashMap 内部使用段(Segment)来表示这些不同的部分。实际上,每个段就是一个小的哈希表,每个段都有自己的锁(Segment 类继承了 ReentrantLock 类)。这样,只要多个修改(写)操作发生在不同的段上,它们就可以并发进行。

ConcurrentHashMap实现线程安全的关键点:

  • Segment类继承了ReentrantLock类,对每个段进行写操作时都会加锁。
  • 在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因
  • ConcurrentHashMap中key和value都不允许为空,但在读操作时有可能会出现键值对存在但读出来的value值为空的情形。这种情形发生的场景是:初始化HashEntry时发生的指令重排序导致的,也就是在HashEntry初始化完成之前便返回了它的引用。这时,JDK给出的解决之道就是加锁重读。
  • size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。

相较于JDK1.7,在JDK1.8中,对ConcurrentHashMap做了较大的改动,主要有两方面:

  • 取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
  • 将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。

 

参考 https://www.cnblogs.com/be-forward-to-help-others/p/6708130.html

 

 

栈基于数组实现:                         手写实现一个栈类:

public class Stack {

    private int maxSize; //栈的大小
    private int top;     //栈顶的索引号
    private char[] arr;

    public Stack(int size){
        maxSize = size;
        top = -1;
        arr = new char[maxSize];
    }

    //入栈
    public void push(char value){
        arr[++top] = value;
    }

    //出栈
    public char pop(){
        return arr[top--];
    }

    //访问栈顶元素
    public char peek(){
        return arr[top];
    }

    //栈是否为空
    public boolean isEmpty(){
        return top == -1;
    }

}

 

5.子类继承父类时,父类的构造方法什么时候调用

 父类的构造方法不会被子类继承,它们只能使用关键字super从子类的构造方法中调用

调用父类构造方法必须使用关键字super , 调用语法:

          super() 或 super(参数)      (无参构造或有参构造)

//例如B继承自A,假设B想调用A的有参构造方法A(int b, String s)
public B (int a, int b, String s) {
    super(b, s);  //调用父类构造方法
    this.a = a;
}

注意:虽然父类构造方法不会被子类继承,但创建一个子类对象会调用其父类的构造方法

例:

package simplejava;

//父类
class Super {
    String s;
    public Super() {
        System.out.println("Super");
    }
}

//子类
class Sub extends Super {
    public Sub() {
        System.out.println("Sub");
    }

}

public class Q {
    public static void main(String[] args) {
        Sub s = new Sub();
    }
}

输出:

Super
Sub

当一个类继承了某个类时,在子类的构造方法里,super()必须先被调用;如果你没有写,编译器会自动调用super()方法,即调用了父类的默认无参构造方法

这并不是创建了两个对象,其实只有一个子类Sub对象;之所以需要调用父类的构造方法是因为在父类中,可能存在私有属性需要在其构造方法内初始化;

 

注意:出现错误信息:Implicit super constructor is undefined for default constructor

对于子类来说,不管是无参构造方法还是有参构造方法,都会默认调用父类的无参构造方法;当编译器尝试在子类中往这两个构造方法插入super()方法时,因为父类没有一个默认的无参构造方法,所以编译器报错。 (在Java中,如果一个类没有定义构造方法,编译器会默认插入一个无参数的构造方法;但是如果一个构造方法在父类中已定义,在这种情况,编译器是不会自动插入一个默认的无参构造方法,这正是以上demo的情况)

 要修复这个错误有以下几种选择:

1、在父类手动定义一个无参构造方法

public Super(){
    System.out.println("Super");
}

2、在子类中自己明确写上父类构造方法的调用;如super(value);

这样就不会报错。

3.移除父类中自定义的构造方法。

 

参考https://www.cnblogs.com/chenpi/p/5486096.html#_label0

6.static修饰变量、代码块时什么时候执行?执行几次?

 在类加载的init阶段,类的类构造器中会收集所有的static块和字段并执行static块只执行一次由JVM保证其只执行一次

public class TestStatic{    
    public static String name = "";  
      
    static{    
       System.out.println("init ....");    
       name = "admin";  
    }    
      
    public static String getName(){  
        return name;  
    }  
      
    public static String getIdAndName(int id){  
        return id + "---" + name;  
    }  
        
    public static void main(String[] args) {    
        String name = TestStatic.getName();  
        String idAndName = TestStatic.getIdAndName(888);  
        System.out.println(name);  
        System.out.println(idAndName);  
    }    
        
}    
  
对执行结果分析:  

  init ....
  admin
  888---admin

    在调用TestStatic类中任何一个方法时,jvm进行类加载,static语句块是在类加载器加载该类的最后阶段进行初始化的。并且只会被初始化一次。  

    若一次性调用多个方法,则只会执行一次static代码块。  

  
说明:static语句块,不是在实例化的时候被执行的

 

static代码块的使用 :

1、项目对某些数据进行初始化,可以在两个地方处理。 
     第一、就是在项目启动时,加载某个类,对数据进行数据化(如:初始化基础数据或数据库连接池)。 
     第二、就是在某个工具类中使用static静态代码块,当第一次访问工具类时,就会先进行初始化(只会执行一次),保存到静态全局属性中,当其他类再次访问时,将直接使用初始化数据(如:连接redis数据库,并初始化连接池)。 

2、缓存数据

 

 

 

参考https://huangliangbao.iteye.com/blog/2217362

 

posted on 2019-05-29 23:07  Toria  阅读(146)  评论(0编辑  收藏  举报

导航