从牛客网上看到的一些面试题答案收集与整理

边写边学,有待提高

1、请你简单描述一下正则表达式及其用途。

在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具

2、Java中是如何支持正则表达式操作的。

Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作,如:

String testString = "abcabcabcdefabc";
String [] regexs = new String []{"abc+","(abc)+","(abc){2,}"};

    for (String regex: regexs) {
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(testString);
        System.out.println("test regex: " + regex);
        while(m.find()){
            System.out.println("match " + m.group() + " at position " + m.start() + "-" + (m.end() -1));
        }
    }

输出结果:

  test regex: abc+
  match abc at position 0-2
  match abc at position 3-5
  match abc at position 6-8
  match abc at position 12-14
  test regex: (abc)+
  match abcabcabc at position 0-8
  match abc at position 12-14
  test regex: (abc){2,}
  match abcabcabc at position 0-8

几个正则表达式的含义:

	abc+:匹配abc或者abcc或者abccc等。

  (abc)+:根据贪婪原则,匹配1次或者多次连续的abc,匹配最长的字符串。

  (abc){2,}:abc至少连续出现2次,匹配abcabc或者abcabcabc等。

测试一个字符串是否匹配某个正则表达式,可以使用下面的方法:

String testString = "abcabcabcdefabc";
System.out.println(Pattern.matches("abc+", testString));
System.out.println(Pattern.matches("abc+", "abccc"));

输出结果为:false 和 true。

3、请你比较一下Java和JavaScript。

JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript;而Java的前身是Oak语言。
下面对两种语言间的异同作如下比较:

基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
解释和编译:Java的源代码在执行之前,必须经过编译。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript的运行效率)
强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
代码格式不一样

4、请你介绍一下Java中如何跳出当前的多重嵌套循环

在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。
(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好)

public static void main(String[] args) {
        int count = 0;
        A:
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {

                if (i == 5) {
                    System.out.println("count = " +count);
                    break A;
                }
                count++;
            }
        }
    }

输出结果:

    count = 50

5、请你讲讲&和&&的区别。

&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式不会进行运算。

例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。

6、int和Integer有什么区别?什么是自动拆装箱?

Java是一种近乎纯洁的面向对象编程语言,但为了编程方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当做对象来操作,Java为每个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类型为integer,从Java 5开始引入了自动拆装箱机制,使得两者可以互相转化。

Java为每个基本数据类型都提供了包装类型:

原始类型: booleancharbyteshortintlongfloatdouble

包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
Integer a= 127 与 Integer b = 127 相等吗
对于对象引用类型:==比较的是对象的内存地址。 
对于基本数据类型:==比较的是值。

如果整型字面量的值在-128 到 127 之间,那么自动装箱时不会 new 新的 Integer 对象, 而是直接引用常量池中的 Integer 对象,超过范围 a1==b1 的结果是 false.

public static void main(String[] args) {
        Integer a = new Integer(3);
        Integer b = 3;  //将3自动装箱成Integer类型
        int c = 3;

        System.out.println(a == b); // false 两个引用没有引用同一对象
        System.out.println(a == c); // true a 自动拆箱成 int 类型再和 c 比较
        System.out.println(b == c); // true

        Integer a1 = 128;
        Integer b1 = 128;
        System.out.println(a1 == b1); // false   生成新的Integer对象

        Integer a2 = 127;
        Integer b2 = 127;
        System.out.println(a2 == b2); // true   直接从常量池拿
    }

7、请你讲讲如何输出一个某种编码的字符串?如iso8859-1等

public static String tranStr() {
       String tempStr = "";
       try {
           tempStr = new String(tempStr.getBytes("ISO-8859-1"), "GBK");
           tempStr = tempStr.trim();
       }
       catch (Exception e) {
           System.err.println(e.getMessage());
       }
       return tempStr;
   }

8、说一下string、stringBuffer和stringBuilder的区别。

可变性

string中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。
stringBuffer和stringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder类中用同样使用字符数组保存字符串,不同的是在AbstractStringBuilder类中的字符数组char[] value没有用final关键字修饰,这两种对象都是可变的。

线程安全性

string中的对象是不可变的,也就是常量,是线程安全的。
AbstractStringBuilder类是StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
Stringbuffer 对方法重写时加了同步锁,或对调用的方法加了同步锁(synchronized),是线程安全的;
StringBuilder 并没有对方法进行加 同步锁,所以是非线程安全的。

性能

对string对象每次进行改变时,都会新生成一个对象,然后将指针指向新的string对象;
StringBuffer每次都会对StringBuffer对象本身进行操作,不是新生成对象并改变引用;
相同情况下使用 StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。因为StringBuffer 加了synchronized关键字。

StringBuffer 中还存在toStringCache字段,注解的含义是 toString返回的最后一个值的缓存,在修改StringBuffer时清除。在大部分方法中都会先将toStringCache设为null。

/**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

9、请你谈谈大O符号(big-O notation)

大O符号表示性能在渐进意义(n–>无穷)下的最坏情况。
大O描述当数据结构中的元素增加时,算法的规模和性能在最坏情景下有多好。
一般可以描述一个算法的时间复杂度和空间复杂度。

10、请你讲讲数组(Array)和列表(ArrayList)的区别?什么时候应该使用Array而不是ArrayList?

区别

Array的长度是固定的,ArrayList的长度是动态变化的;
Array可以包含基本数据类型和对象类型,ArrayList只能包含对象类型;
ArrayList提供了更多的方法和特性,比如addAll(),removeAll(),iterator()等等;
对于基本数据类型,集合采用自动装箱机制来减少编码工作量,当处理固定大小的基本数据类型时,这种效率较慢。

应用场景

当长度不固定时,采用ArrayList;
当长度固定且为基本数据类型时,采用Array,不需要自动拆装箱。


11、请你解释值传递和引用传递。

Java中只有值传递
值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
引用传递是对对象变量类型而言的,传递的是对象地址,所以引用对象改变时原对象也改变。

		int a = 5;
        int b = a;
        b= 4;
        System.out.println("a:"+ a);
        System.out.println("b:"+ b);
    输出	
    	a:5
   		b:4
class Person{
    	private String name;
        private int age;
   }
		Person p1 = new Person();
        p1.setName("name1");
        p1.setAge(11);
        Person p2 = p1;
        p2.setName("name2");
        p2.setAge(12);
        System.out.println("p1name :" + p1.getName() + " p1age :" + p1.getAge());
        System.out.println("p2name :" + p2.getName() + " p2age :" + p2.getAge());
	输出
	p1name :name2 p1age :12
	p2name :name2 p2age :12

12、请你解释为什么会出现4.0-3.6=0.40000001这种现象?

2进制的小数无法精确的表示10进制的小数,计算机对10进制小数进行运算时要先转化成2进制小数,这个过程中出现了误差。
10进制的数在内存中通过补码的形式进行存储,正数的原反补码相同,负数的反码是原码除符号位的其余位取反,补码是反码+1。

13、请你说说Lambda表达式的优缺点。

先简单看一下什么是Lambda表达式。
Lambda表达式也可称为闭包,它是推动Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数。lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

 	public interface vo{
        void convert(int i);
    }
    
		int a = 1;
        vo b = (n)-> System.out.println( a + 2 );
        b.convert(1);
输出结果为:3
		int a = 1;
        vo b = (n)-> System.out.println( a + 2 );
        b.convert(1);
        a=2;
此时无法通过编译,因为lambda表达式中的参数在上下文有变化。

优点:简洁,容易并行计算,可能代表未来的编程趋势。
缺点:若不用并行计算,很多时候计算速度没有比传统的for循环快(并行计算有时需要预热才显示出效率优势),不容易调试,若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂。

14、说说Java 8的新特性

1、函数式接口

Java 8引入的一个核心概念是函数式接口(Functional Interfaces),通过在接口中添加一个抽象方法,这些方法可以直接在接口中运行,如果一个接口中定义了唯一一个抽象方法,那么这个接口就是函数式接口。同时引入了一个新的注解:@FunctionalInterface 。可以把它放到一个接口前,表示这个接口是一个函数式接口。这个注解时非必须的,只要接口中只定义了一个抽象方法,虚拟机会自动判断,不过最好在接口上使用注解 @FunctionalInterface 进行声明。如果在接口中添加了@FunctionalInterface 注解,只允许有一个抽象方法,否则会报错

2、Lambda表达式

函数式接口的重要属性是:我们能使用Lambda表达式来实例化它们,Lambda表达式能让你讲函数作为方法的参数,或将代码作为数据对待。Lambda表达式的引入给开发者带来了很多优点:在Java 8之前,匿名内部类,监听器和事件处理器的使用都显得很冗长,代码可读性很差,Lambda表达式的应用使得代码变得很紧凑,可读性增强;Lambda表达式使得并行操作大集合变得很方便,可以充分发挥多核CPU的优势,更易于为多核处理器编写代码。

3、集合之流式操作(Stream)

Java 8引入了流式操作(Stream),通过该操作可以实现对集合(Collection)的并行处理和函数式操作。根据操作返回的结果不同,可以将流式操作分为中间操作和最终操作两种。最终操作返回一特定类型的结果,中间操作返回流本身,这样就可以将多个操作依次串联起来。根据流的并发性,流可以分为串行和并行两种。流式操作实现了集合的过滤、排序、映射等操作。
Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,而Stream 是有关计算的。前者主要面向内存,储存在内存中,后者主要面向CPU,通过CPU实现计算。
串行和并行的流
流有串行和并行两种,串行流的操作是在一个线程中依次完成的,并行流的操作是在多个线程上同时执行。串行流和并行流可以相互切换:通过 stream.sequential() 返回串行的流,通过 stream.parallel() 返回并行的流。并行流可以很大程度上提高程序的执行效率。

中间操作:
该操作会保持 stream 处于中间状态,允许做进一步的操作。它返回的还是的 Stream,允许更多的链式操作。常见的中间操作有:

filter():对元素进行过滤;

sorted():对元素排序;

map():元素的映射;

distinct():去除重复元素;

subStream():获取子 Stream 等。

最终操作:
该操作必须是流的最后一个操作,一旦被调用,Stream 就到了一个终止状态,而且不能再使用了。常见的终止操作有:

forEach():对每个元素做处理;

toArray():把元素导出到数组;

findFirst():返回第一个匹配的元素;

anyMatch():是否有匹配的元素等。

15、"=="和"equals"的区别

“==” 比较两个对象时,比较的是对象的内存地址是否相等,如果两个对象引用完全相同,则该操作符返回true。

“==” 比较两个基本数据类型时,比较的是值是否相等。
比较String时要使用equals。

equals方法是基类Object中的实例方法,因此对所有继承于Object的类都会有该方法,如果不重写equals方法,比较的是引用是否相同。
Object类中equals方法的实现:

public boolean equals(Object obj) {
        return (this == obj);
    }

重写equals方法主要分3步骤:
1、先比较引用是否相同(是否为同一对象)
2、再比较是否为同类型
3、最后比较内容是否一致

16、equals和hashCode的联系。

两个对象equals比较为true,他们的hashCode一定相等;
两个对象的hashCode相等,他们的equals比较不一定为true;
重写equals方法,一定要重写hashCode方法;Object若不重写hashCode()的话,Object 的 hashCode方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法直接返回对象的内存地址。

17、Map的分类和常见情况。

Map有四个主要常用的实现类,不是只有4个实现类

1、HashMap

JDK7中HashMap的底层实现是数组+链表;JDK8中HashMap的底层实现是数组+链表+红黑树。
JDK8中如果哈希单向链表中的节点数超过8个,则会转化为红黑树结构;如果红黑树上的节点数小于6个,则会转化为单向链表结构。
HashMap(JDK8)的put方法
原理:
1、先计算key的hash值
2、根据hash值找到数组下标
3、如果数组为空则插入,否则往链表尾部添加元素

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

put方法调用了一个putVal的方法,这个方法的目的就是将值放到对应的位置,这个对应的位置是怎么找到的呢?

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i; //定义一个tab数组,n是当前table数组长度,i是当前table数组下标
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//这个if是当数组为null或长度为0时进行初始化操作resize
        if ((p = tab[i = (n - 1) & hash]) == null)//(n - 1) & hash]查找下标,查找这个位置是否是空
            tab[i] = newNode(hash, key, value, null);//如果为空时则调用newNode构造函数直接new出来,next指向null
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//当添加的元素key值相同时,将p赋值给此时新建节点e
            else if (p instanceof TreeNode)//如果是树节点,则增加到树节点上
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//如果当前节点的next指向null
                        p.next = newNode(hash, key, value, null);//将p节点指向新添加的节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//如果节点数超过TREEIFY_THRESHOLD ,则进行树化
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//如果找不到指向null节点的则继续循环
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;//如果e的节点不为空的话返回e的value作为oldValue
            }
        }
        ++modCount;
        if (++size > threshold)//如果大于这个值就进行扩容操作;2倍扩容。
            resize();
        afterNodeInsertion(evict);
        return null;
    }

首先,获取key的hash值,调用如下方法

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

解释一下,当key是null的时候,key的hash值为0;不为null时,先获取key的hashCode赋值给h,再将h向右移动16位,进行异或运算。得出hash值。这是是异或运算符 ^ ,而不是 & 和 | 运算符呢?

0 0 1 1
0 1 0 1
当进行&运算时,只有两者都为1才是1,得出的结构会更加偏向0   0:1的概率是3:1
当进行|运算时,只要有1就是1,得出的结果更偏向1   0:1的概率是1:3
当进行^运算时,两者相同为0,不同为1,得出的结果更加公平   0:1的概率是1:1

接下来是获取put进来的元素的数组下标,(n - 1) & hash (n代表数组长度),还有一种方法是hash%n,但是效率不如前者,JDK8中使用的是前者;获取下标后将该对象插入数组。
假如插入的两个key的数组下标相同,这种情况叫做哈希冲突(也叫哈希碰撞),此时将会形成链表,将后插入的数据插入到链表的尾部(JDK7是头部插入),新增元素后如果hashMap的容量达到threshold进行2倍扩容;扩容后,将重新计算链表中节点的位置;数组长度n变大。
查找的原理:
1、先计算key的hash值
2、根据hash值找到数组下标
3、如果找到则返回,否则遍历链表找到对应节点
删除的原理:
1、先计算key的hash值
2、根据hash值找到数组下标
3、如果找到则删除,否则遍历链表找到对应节点,然后删除节点
HashMap是线程不安全的,如果多线程环境建议使用ConcurrentHashMap

2、LinkedHashMap

是HashMap的一个子类,但它保持了记录的插入顺序,遍历时先插入的先得到,也可以在构造时带参数,按照应用次数排序,遍历时没有HashMap快。
不过有个例外,当HashMap的容量很大,实际数量很小时,遍历起来会比LinkedHashMap慢,因为HashMap的遍历速度和它的容量有关,LinkedHashMap遍历速度只和数据多少有关。

3、TreeMap

实现了sortMap接口,能够把保存的记录按照键排序(默认升序),也可以指定排序比较器,遍历时得到的数据是排过序的。

4、HashTable

HashTable和HashMap比较相似,但是它继承Dictionary类,而且是线程安全的;但是它的写入速度比较慢,不允许键或值为null。

18、将你讲讲final,finally,finalize的区别。

final用于修饰类、属性和方法。

被final修饰的类不能被继承。
被final修饰的方法不能被重写。
被final修饰的变量,如果是基本数据类型,一旦初始化则无法改变;如果是引用类型的变量,则在初始化之后无法指向另一个对象。

finally用于异常处理。

finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。这句话有一定的问题,假如在try/catch语句中存在System.exit(0) 方法,将不会执行finally中的语句。

finalize

finalize是Object类中定义的,也就是说每个对象中都存在这个方法。这个方法在gc启动,该对象被回收时调用,其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。

特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。
使用finalize还需要注意一个事,调用super.finalize();

一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。 所以,推荐不要使用finalize()方法,它跟析构函数不一样。

19、将你讲讲synchronized和lock。

类别synchronizedlock
存在层次Java的关键字,在jvm层面上是一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式
锁的状态无法判断可以判断
锁的类型可重入 不可中断 非公平可重入 可中断 可公平/非公平(两者皆可)
锁的性能少量同步大量同步

晚上接着写

20、将你讲讲volatile关键字。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值