java基础面试题学习理解

123 阅读13分钟

1.前言

临近年关,新的一年又要来了,马上要到求职的黄金阶段了,提前准备复习下面试题是在所难免的。但好多时候准备面试的时候会忽略一些简单的基础只是,或者对于基础只是理解不够深入,其实不管是面试或者以后工作中,基础知识都是很重要和关键的。时常复习和深入理解是很有必要的。下面是我整理的一些java基础知识面试题和我自己的一些理解,在这里和大家一起学习,如果有错误和自己的想法欢迎指出和讨论,一起学习一起进步。

2.基础面试题

2.1 java中==和equals和hashCode的区别

这道题其实大家都应该清除或者了解的比较多,在这里再次复习下。

  • 首先我们知道java中的数据类型分为基础数据类型和引用数据类型,基础数据类型有八种,byte、short、int、long、float、double、char、boolean。引用类型指的是持有的值只是对象的引用地址。
  • ==是运算符,比较两个变量的值或者引用对象地址是否相等,也就是说对于基本数据类型是可以直接比较。
  • equals是比较两个对象是否相等,我们来看下面这个实例
   public static void main(String[] args) {
        int a = 1;
        int b = 1;
        System.out.println(a == b);

        String c = new String("1");
        String d = new String("1");
        System.out.println(c == d);
        System.out.println(c.equals(d));

    }
    
    true
    true
    false
    true

可以看到a,b是基本数据类型,值相等即可,c和d都是new出来的对象,是两个不同的对象所以,c==d未flase,c.equals(d)相等时因为String重写了equals方法,只比较其对象值相等即可相等,这个可以渠看看String的源码。所以如果自定义对象需要比较其是否相等需要重写equals方法,自定义相等逻辑。

  • hashcode一般是和equals一起出现的,hashcode返回的是一个对象的哈希码值,如果两个对象通过equals方法比较相等,那么他的hashCode一定相等;如果两个对象通过equals方法比较不相等,那么他的hashCode有可能相等;
  • 重写equals为什么需要重写hashcode,如果之重写equals,equals出来时true但是hashcode的值有可能是不一样的,比如set,如果重复就会进行覆盖,但是如果只是equals相同,hashcode不同的话,进行数据覆盖会出现相同的对象因为hashcode而不能进行覆盖处理。

2.2 int与integer的区别

  • 首先int是基本数据类型,integer是引用数据类型。
  • integer是int的包装类型,是一个对象
  • Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向对象,而int则直接存储数值
  • Integer的默认值是null,而int的默认值是0。
  • Integer用eqquals比较,int用==比较。
public static void main(String[] args) {
        int a = 1;
        int b = 1;
        System.out.println(a == b);

        Integer c = new Integer(1);
        Integer d = new Integer(1);
        System.out.println(c == d);
        System.out.println(c.equals(d));

        Integer e = 128;
        Integer f = 128;
        System.out.println(e == f);
        System.out.println(e.equals(f));

        Integer m = 127;
        Integer n = 127;
        System.out.println(m == n);
        System.out.println(m.equals(n));
    }
    
true
false
true
false
true
true
true

需要注意的是Integer直接赋值,如果大小在-127~128,会直接指向缓存对象所以用==也是true的,如果不在这个范围就是新new对象,两个对象的地址是不想通过的。

2.3 java多态的理解

  • 什么是多态,我的理解是同一类物品有不同的形态,比如动物有小鸟,大象,狮子等。代码层面的理解就是不同的对象对于同一方法有不同的处理方式和实现。
  • 多态存在的三个必要条件,继承,重写,父类引用指向子类应用。
  • 多态是可以把同一类或者同一行为抽象出来,由父类来定义,子类继承父类重写他的行为或者属性等。这样对于同一类相同的属性和行为由父类定义,可以优化代码的数量,不产生冗余代码。
  • 多态的好处是增加子类不影响原子类和分类,又能很轻松的继承父类的功能和属性。
public class Food {

    private String name ="食物";

    public String getName(){
        return this.name;
    }

}

public class Fruit extends Food {

    private String name ="水果";

    public String getName(){
        return this.name;
    }

}


public static void main(String[] args) {
        Food food = new Fruit();
        System.out.println(food.getName());
}

输出
水果

子类的getName()方法重写了父类中的方法,在main中,父类的引用food指向了子类的对象Fruit,当我们调用food的getName()方法时,实际上是动态绑定到了子类的getName()上,所以会打印出"水果"

2.4 String、StringBuffer、StringBuilder区别

这个问题很简单,大家都应该知道了,在这里在回顾下。

  • 首先String、StringBuffer、StringBuilder都是字符串对象,用于字符串的操作。
  • String有个很关键的地方,它的源码是用flnal修饰的,意味着这是一个不可被继承,也不可变的字符串对象,一但创建就不可以在进行修改。但是为什么我们还没像下面这样写呢
String a="a";
a="b";

这不是对String进行了修改吗,其实不是的,当我们进行赋值的时候其实是生成了一个新的字符串"b",把变量a的指针指向b,真正的字符串"a"是被弃用了,等待gc回收,所以不是对原有对象进行修改。这压根会出现一个问题,当我们频繁的修改字符串或者赋值就会新增很多新的字符串对象,并且把指针进行重定向。这样其实效率很低也会产生垃圾字符串,降低内存。
  • 所以出现了可变字符串对象StringBuffer和StringBuilder,这两个对象就是可以进行任意修改和赋值的,StringBuffer是线程安全的,StringBuilder非线程安全,所以StringBuilder的效率要高于StringBuffer。根据不同的场景可以灵活的选用。
  • String 可以直接赋值初始化,StringBuffer和StringBuilder必须new出来。
  • 当对字符串的修改操作不多时,可以选用String
  • 存在频繁修改操作需要使用StringBuffer和StringBuilder效率更高,不会产生额外的内存消耗。
  • 多线程需要使用StringBuffer保证线程安全,单线程使用StringBuilder效率更高。

2.5 什么是内部类?内部类的作用?

  • 首先我们先了解下什么是内部类 字面意思,将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。内部类又分为几种。
  • 成员内部类 也是字面意思定义在外部类的成员位置就是成员内部类
public class Outer {
    private int age = 20;
    //成员位置
    public class Inner {
        public void show() {
            System.out.println(age);
        }
    }
}

特点:成员内部类可以使用外部类中所有的成员变量和成员方法(包括private的)。
如果成员内部类被定义为private,这样的类不可以通过new来创建和调用,只有外部类能够调用,可以提供一个public方法返回内部类的实现来实现调用。

  • 局部内部类 定义在一个方法或者一个作用域里面的类
public class Outer {
    public void method(){
        class Inner {
        }
    }
}

//在局部位置,可以创建内部类对象,通过对象调用和内部类方法
public class Outer {
    private int age = 20;
    public void method() {
        final int age2 = 30;
        class Inner {
            public void show() {
           	    System.out.println(age);
                //从内部类中访问方法内变量age2,需要将变量声明为最终类型。
                System.out.println(age2);
            }
        }
        
        Inner i = new Inner();
        i.show();
    }
}

为什么局部内部类访问局部变量必须加final修饰呢?
因为局部变量是随着方法的调用而调用,使用完毕就消失,而堆内存的数据并不会立即消失。
所以,堆内存还是用该变量,而该变量已经没有了。为了让该值还存在,就加final修饰。
原因是,当我们使用final修饰变量后,堆内存直接存储的是值,而不是变量名。

主要是作用域发生了变化,只能在自身所在方法和属性中被使用。

  • 静态内部类 用static修饰的内部类
public class Outter {
    int age = 10;
    static age2 = 20;
    public Outter() {        
    }
     
    static class Inner {
        public method() {
            System.out.println(age);//错误
            System.out.println(age2);//正确
        }
    }
}

不能使用外部类的非static成员变量和成员方法。其实这个也很好理解,静态内部类可以看作是外部类的一个静态成员,不需要外部类的实例也能够直接访问静态内部类,而外部类的非static成员必须依赖于对象的调用,静态成员则可以直接使用类调用,不必依赖于外部类的对象,所以静态内部类只能访问静态的外部属性和方法。

  • 匿名内部类 一个没有名字的类,是内部类的简化写法。
public interface Inter {
	public abstract void show();
}

public class Outer {
    public void method(){
        new Inner() {
            public void show() {
                System.out.println("HelloWorld");
            }
        }.show();
    }
}

public static void main(String[] args)  {
    	Outer o = new Outer();
        o.method();
}

简单理解其实是继承该类或者实现接口的子类匿名对象。也就是说一个没有名字但是继承了父类或者实现了接口的一个类。

  • 内部类的作用其实理解了什么是内部类就清楚了,内部类可以简化代码和简化类的数量,只是需要我们在不同应用场景选择不用的内部类即可。

2.6 什么是闭包

闭包是引用了自由变量的函数,这个被引用的变量将和这个函数一同存在。通俗的来说闭包能够将一个方法作为一个变量去存储,这个方法有能力去访问所在类的自由变量。来看下面这个例子。

public class Milk {
	
	public final static String name = "纯牛奶";//名称
	
	private static int num = 16;//数量
	
	public Milk()
	{
		System.out.println(name+":16/每箱");
	}
	
	/**
	 * 闭包
	 * @return 返回一个喝牛奶的动作
	 */
	public Active HaveMeals()
	{
		return new Active()
				{
					public void drink()
					{
						if(num == 0)
						{
							System.out.println("木有了,都被你丫喝完了.");
							return;
						}
						num--;
						System.out.println("喝掉一瓶牛奶");
					}
				};
	}
	
	/**
	 * 获取剩余数量
	 */
	public void currentNum()
	{
		System.out.println(name+"剩余:"+num);
	}
}

/**
 * 通用接口
 */
interface Active
{
	void drink();
}
使用上述实现

public class Person {

	public static void main(String[] args) {
		//买一箱牛奶
		Milk m = new Milk();
		
		Active haveMeals = m.HaveMeals();
		
		//没事喝一瓶
		haveMeals.drink();
		//有事喝一瓶
		haveMeals.drink();
		
		//看看还剩多少?
		m.currentNum();
	}

}
运行结果

纯牛奶:16/每箱
喝掉一瓶牛奶
喝掉一瓶牛奶
纯牛奶剩余:14

使用内部类+接口实现用接口方法访问外部类的静态变量。上述实现了喝牛奶的动作。

2.7 java值传递和引用传递的区别

首先我们必须认识到这个问题一般是相对函数而言的,也就是java中的方法参数。

  • 值传递,也就是方法接收到的参数是调用者的值。java中的8大基本数据类型是按照值传递的。
  • 引用传递 方法接收到的参数是调用者传递的引用,也就是地址。java中的对象实例,数组,接口是按照引用传递的。 这里我们需要注意的是一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值,这句话相当重要,这是按值调用与引用调用的根本区别。 比如下面的例子,
public static void main(String[] args) {
        int x = 10;
        int y = 20;
        swap(x, y);
        System.out.println("x(2) = " + x);
        System.out.println("y(2) = " + y);
}
public static void swap(int x, int y) {
        int temp = x;
        x = y;
        y = temp;
        System.out.println("x(1) = " + x);
        System.out.println("y(1) = " + y);
}

输出结果
x(1) = 20
y(1) = 10
x(2) = 10
y(2) = 20

可以很明显的看到,调用方法前后x,y的值是没有任何变化的,这就解释了上面说的一个方法不能修改按值传递调用所对应变量的值。原因很简单,对于上述例子java的处理模式是,在进行方法调用的时候,会对参数x,y进行一个拷贝,交换的也是拷贝的值,真正的x,y是没有做任何操作的。当方法调用完成后就会对拷贝的x,y进行回收,最后x,y的值没有进行交换。

  • 问题又来了,为什么引用传递就能修改调用者的值呢?我们再来看一个列子
public class User {
	private String name;
	private int age;
	public User(String name, int age) {
		this.name=name;
		this.age=age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

public class CallByValue {
	private static User user=null;
	public static void updateUser(User student){
	student.setName("Lishen");
	student.setAge(18);
}
	
	
public static void main(String[] args) {
	user = new User("zhangsan",26);
	System.out.println("调用前user的值:"+user.toString());
	updateUser(user);
	System.out.println("调用后user的值:"+user.toString());
}

输出结果
调用前user的值:User [name=zhangsan, age=26]
调用后user的值:User [name=Lishen, age=18]

为什么调用前后user的值呗改变了呢,我们来理一下他的执行步骤。
1.user为User对象的一个实例化。
2.打印user,这时候为User [name=zhangsan, age=26]。
3.进行方法调用,因为是引用传递,student为user的拷贝,也就是User对象的地址。这时候user和student指向同一个对象。
4.对student重新赋值,也就是操作原User对象,所以,最终调用完成后,User对象被修改为User [name=Lishen, age=18]。 再来看下另一个例子:

public class CallByValue {
	private static User user=null;
	private static User stu=null;
	
	/**
	 * 交换两个对象
	 * @param x
	 * @param y
	 */
	public static void swap(User x,User y){
		User temp =x;
		x=y;
		y=temp;
	}
	
	
	public static void main(String[] args) {
		user = new User("user",26);
		stu = new User("stu",18);
		System.out.println("调用前user的值:"+user.toString());
		System.out.println("调用前stu的值:"+stu.toString());
		swap(user,stu);
		System.out.println("调用后user的值:"+user.toString());
		System.out.println("调用后stu的值:"+stu.toString());
	}
}

输出结果
调用前user的值:User [name=user, age=26]
调用前stu的值:User [name=stu, age=18]
调用后user的值:User [name=user, age=26]
调用后stu的值:User [name=stu, age=18]

既然是引用传递为什么最终两个对象没有进行交换,值还是不变呢。其实这个和第一个例子其实是一样的,最终还是交换的拷贝对象实例的引用对象,但是对于原来的实例其实指向的还是原来的对象,所以没有交换成功。
这个例子说明了一个问题,java程序设计语言对对象采用的不是引用调用,实际上是对象引用进行的是值传递,当然在这里我们可以简单理解为这就是按值调用和引用调用的区别。

  • 总结 1.一个方法不能修改一个基本数据类型的参数(数值型和布尔型)。
    2.一个方法可以修改一个引用所指向的对象状态,但这仍然是按值调用而非引用调用。
    3.java的设计理念中,做不到对变量的原始值进行交换。
    4.上面两种传递都进行了值拷贝的过程。