欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

面试系列(四):Java基础知识

程序员文章站 2022-05-06 18:48:21
...

基础也是Java面试里最基本的考查~ 下面就以我遇到的较为常见的点来整理,比较零散,仅供参考。

 

1、switch……case

要注意case之间要用break来分隔,否则将会一直执行下去直到有break的地方:

public static void switchTest(int i) {
		switch(i) {
			case 1:
				System.out.println("============1");
				break;
			case 2:
				System.out.println("============2");
//				break;
			case 3:
				System.out.println("============3");
				break;
			default:
				System.out.println("==========more");
		}
	}

 case 2的时候,break是注释的,此时调用:

for(int i=0; i<5; i++) {
	switchTest(i);
}

 打印结果为:

==========more

============1

============2

============3

============3

==========more

default为所有case都不满足的时候走的逻辑;因为case 2的时候没有break,所以case 2的时候走完2的逻辑也会走case 3的逻辑。而如果把注释的break放开,打印结果将会是:

==========more

============1

============2

============3

==========more

注意:在jdk1.7+后,case里不仅支持整型,也支持字符串。当case判断条件为整型且为变量时,需注意该变量一定是final int的~

 

2、Java异常处理机制

1)异常的分类

Java中用Throwable类代表异常,而Throwable类会派生出Exception和Error两个子类。

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题;

Exception(异常):是程序本身可以处理的异常。

 

运行时异常:都是RuntimeException类及其子类异常,也称非检查异常,是指在编译期间,不检查这类异常是否捕获。如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

 

throw主动抛出异常;throws声明异常(在方法定义上),则调用此方法的地方必须捕获或者继续throws~

 

2)需要注意的点

2.1:如果一段代码用try{} catch(){} 包围起来,就算发生了异常,但catch后面的代码还是会执行;如果在for循环中,try catch把整个for都包起来,那么循环到某次发生异常时,整个循环就会中断;如果把try catch写在for里面包围每次循环的内容,那么本次如果发生异常将不会影响整个循环的继续进行。

 

2.2:try后面可以跟多个catch,最好在所有特殊异常最后写一个*的父类异常:Exception必须写在最后,其子类写在前面。当所有子类异常都没有catch到它时,就会进入Exception的分支。

 

2.3:当try catch finally遇上return

原则:任何执行try 或者catch中的return语句之前(无论try或catch里面有没有被return掉),都会执行finally语句,如果finally存在的话。 如果finally中有return语句,那么程序就以finally里的return为准,否则以try或catch里的return为准。

public static void main(String[] args) {
		System.out.println("调用结果:" + test());
		
	}
	
	static int test()
	{
		int x = 1;
		try
		{
			x++;
			System.out.println("x:" + x);
			System.out.println(x/0);
			return x;
		} catch(Exception e) {
			e.printStackTrace();
			return 0;
		}
		finally
		{
			++x;
			System.out.println("x:" + x);
//			return x;
		}
	}

 打印结果为:

x:2

java.lang.ArithmeticException: / by zero

at com.nineclient.Test5.test(Test5.java:17)

at com.nineclient.Test5.main(Test5.java:6)

x:3

调用结果:0

而把finally的注释放开,结果为:

x:2

java.lang.ArithmeticException: / by zero

at com.nineclient.Test5.test(Test5.java:17)

at com.nineclient.Test5.main(Test5.java:6)

x:3

调用结果:3

说明以finally的return为准了。而如果这样:

static int test()
	{
		int x = 1;
		try
		{
			x++;
			System.out.println("x:" + x);
			return x;
		} catch(Exception e) {
			e.printStackTrace();
			return 0;
		}
		finally
		{
			++x;
			System.out.println("x:" + x);
		}
	}

 打印结果为:

x:2

x:3

调用结果:2

说明即时try里已经return了。finally里的代码还是会继续执行的~但是finally里的++x并不会影响最终return的值,虽然x此时变成3了,但是return的时候还是try里第一次x++后的值。

 

 3、String

1)基本

java.lang.String类的实例用于封装一个字符序列,字符串对象为“不变对象”,一旦对字符串进行修改操作,会创建新的对象。Java中允许我们将一个字面量赋值给字符串引用类型变量,处于性能的考虑,Java对字面量产生的字符串进行了缓存,将他们缓存在字符串的常量池中,对于重复出现的字面量赋值,JVM会先查找常量池中是否存在这个字符串,有就直接引用,减少字符串对象的创建,节省内存资源。

String s = new String("abc");//创建了2个对象,内容都是"abc":一个在字符串常量池,一个在堆里。s只是对象的引用,下同
		  String s1 = "abc";//s1指向字符串常量池里内容为"abc"
		  String s2 = new String("abc");//创建了1个对象,在堆里,内容指向堆里的"abc"
		  String s3 = "abc";//s3指向了字符串常量池里内容为"abc",跟s1一样都指向池里的相同内容
		  
		  System.out.println(s == s1);//false
		  System.out.println(s == s2);//false
		  System.out.println(s1 == s2);//false
		  System.out.println(s1 == s3);//true
		  
		  System.out.println();
		  
		  String o = "o";
		  String k = "k";
		  String ok = "ok";
		  System.out.println(ok == "o" + k);//false, "o"+k会在堆里新生成一个对象
		  System.out.println(ok == "o" + "k");//true, 都是在常量池里,相加也是返回池里的"ok"
                  System.out.println(ok == o + k); //false 

 2)String的封装类

StringBuilder和StringBuffer都是String的封装类,对于字符串的操作建议用这个

StringUtils.countMatch(String str, String subStr);//统计subStr在str里出现的次数

StringBuilder:线程非安全的

StringBuffer:线程安全的

		  String test = "Today" + "is" + "sunny";
		  String test2 = "Todayissunny";
		  String t1 = "Today";
		  String t2 = "is";
		  String t3 = "sunny";
		  System.out.println(test == test2);//true
		  System.out.println(test == (t1+t2+t3));//false

 

 4、基本类型的包装类

数值型:整型—— byte(8位)<short(16位)<int(32位)<long(64位)(按所占字节数从小到大排列,数值范围-2n~2n-1 n为幂) 1字节=8位

浮点型:float(32位)<double(64位) 定义float类型时,如果值是非整数,则需加f后缀。可以将float类型的值赋给double型的变量,反之则不行~

文本型:char——字符常量为单引号括起来的单个字符

布尔型:boolean——true/false

public static void main(String[] args) {
		Integer a = 1;
		int b = 1;
		Integer c = 1;
		Integer d1 = new Integer(1);
		System.out.println(a == b);//true, a会自动"拆箱"变成int
		System.out.println(a ==c);//true
		System.out.println(a ==d1);//false,因为d1指向的是通过new出来的值
		
		//需注意:在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,
		//便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象
		//所有整型都同理(Byte,Short,Long),有自己的cache范围~
		Integer a1 = 200;
		Integer b1 = 200;
		System.out.println(a1 == b1);//false
		System.out.println();
		
		//在某个范围内的整型数值的个数是有限的,而浮点数却不是。所以每次都会创建一个新的Float对象
		//所有浮点类型都同理(Double)
		Float f = 1.0f;
		float f1 = 1.0f;
		Float f2 = 1.0f;
		double d = 1.0;
		System.out.println(f == f1);//true
		System.out.println(f == f2);//false,两个对象 都是Float
		System.out.println(f1 ==d);//true,double自动转型成float
		System.out.println();
		
		Boolean bool1 = true;
		Boolean bool2  = true;
		System.out.println(bool1 == bool2);//true
}

 double/float在计算时会有舍入误差,想要得到更加精确的结果,可以使用java.math.Bidecimal

add(), subtract(), multiply(), divide()

 

5、集合

1)接口

Java的集合框架用Collection*接口来表示,该接口又派生出List和Set两个接口。

List:规定了子类实现的特征为有序且元素可重复,常用实现类有ArrayList和LinkedList。ArrayList使用数组实现,所以更适合读取存储的数据;LinkedList使用链表实现,所以更适合插入、删除元素。

Set:规定了子类实现的特征为无序且元素不可重复。常用实现类有HashSet和TreeSet。HashSet使用散列算法实现的Set集合,而TreeSet则使用二叉树算法实现。

2)集合转为数组

		Set<String> set = new HashSet<String>();
		set.add("3");
		set.add("1");
		set.add("1");
		set.add("2");
		//集合转为数组,list和set都有这个语法
		String[] arr = (String[]) set.toArray(new String[0]);
		System.out.println(Arrays.toString(arr));//[3, 2, 1]

 数组转为集合:Arrays.asList(数组);

3)排序

Collections.sort(list);//一般用于数值排序

Collections.sort(Collection c, Comparator cc);//new Comparator 则必须实现方法compare(T o1, T o2)

4)Map

key不能为空且不能重复。

containsKey(Object key)——查看当前Map中是否包含给定的key

containsValue(Object value)——查看当前Map中是否包含给定的value

 遍历Map的3种方式:遍历所有key,遍历所有键值对Entry,遍历所有value

		//遍历key
		for(String key : map.keySet()) {
			System.out.println("key:" + key + ", value:" + map.get(key));
		}
		//遍历键值对
		for(Entry<String, String> entry : map.entrySet()) {
			System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue());
		}
		//遍历value
		for(String value : map.values()) {
			System.out.println("value:" + value);
		}

 

6、数组

主要是注意一些数组的语法~

		int[] a = {1,2,3,4,5,6};
		int[] b = {4,5,6,7,8};
		int[] c = {11,9,10,13,7};
		//打印
		System.out.println(Arrays.toString(a));
		//排序
		Arrays.sort(c);
		System.out.println(Arrays.toString(c));
		//复制
		int[] a2 = a;
		a2[1]++;
		System.out.println("a:" + Arrays.toString(a) + ", a2:" + Arrays.toString(a2));//相互影响
		int[] d = new int[a.length];
		System.arraycopy(a, 0, d, 0, a.length);//src, srcPos, dest, destPos, length
		System.out.println(Arrays.toString(d));
		int[] d2 = Arrays.copyOf(a, a.length);
		System.out.println(Arrays.toString(d2));
		
		//扩容
		int[] e = new int[a.length + b.length];
		System.arraycopy(a, 0, e, 0, a.length);
		System.arraycopy(b, 0, e, a.length, b.length);
		System.out.println("e:" + Arrays.toString(e));

 还有二维数组等~

 

7、面向对象

几大特性:封装、继承、多态

1)接口和抽象类的区别(选择答出几点即可):

抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

 

抽象类要被子类继承,接口要被类实现。

 

接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现

 

接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

 

抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。

 

抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果

 

抽象类里可以没有抽象方法

 

如果一个类里有抽象方法,那么这个类只能是抽象类

 

抽象方法要被实现,所以不能是静态的,也不能是私有的。

 

2)重载和重写的区别

重写,是指子类重新定义父类的虚函数的做法。它是覆盖了一个方法并且对其重写,以求达到不同的作用。重写要注意以下的几点: 

1、重写的方法的标志必须要和被重写的方法的标志完全匹配,才能达到覆盖的效果; 

2、重写的方法的返回值类型必须和被重写的方法的返回一致; 

3、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类; 

4、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。 

 

 

重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。它是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,在使用重载要注意以下的几点: 

1、在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int,float),但是不能为fun(int,int)); 

2、不能通过访问权限、返回类型、抛出的异常进行重载; 

3、方法的异常类型和数目不会对重载造成影响; 

4、对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。

 

3)访问修饰符范围

访问级别

访问控制修饰符

同类

同包

子类

不同的包

公开

public

受保护

protected

--

默认

没有访问控制修饰符

--

--

私有

private

--

--

--

 

 

8.final

final修饰的方法不可以被重写(但可以重载哈~),final修饰的类不可以有子类。

当final修饰基本变量类型时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变 。

当final修饰引用类型变量时,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但是这个对象(对象的非final成员变量的值可以改变)完全可以发生改变。

final修饰的类成员,程序员最好显示地指定其初始值;使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。但是只能初始化一次。

(注:全局变量,即成员变量会被默认赋值;而局部变量的值必须由程序员显式初始化)

 

9、常见设计模式

1)工厂模式

这里直接从拓展性最好的抽象工厂模式说起~

public interface Sender {  
    public void Send();  
}  

//两个实现类:
public class MailSender implements Sender {  
    @Override  
    public void Send() {  
        System.out.println("this is mailsender!");  
    }  
}  

public class SmsSender implements Sender {  
  
    @Override  
    public void Send() {  
        System.out.println("this is sms sender!");  
    }  
}  

//两个工厂类:


public class SendMailFactory implements Provider {  
      
    @Override  
    public Sender produce(){  
        return new MailSender();  
    }  
}  

public class SendSmsFactory implements Provider{  
  
    @Override  
    public Sender produce() {  
        return new SmsSender();  
    }  
}  
//再提供一个接口:


public interface Provider {  
    public Sender produce();  
} 

 测试类:

public class Test {  
  
    public static void main(String[] args) {  
        Provider provider = new SendMailFactory();  
        Sender sender = provider.produce();  
        sender.Send();  
    }  
} 

 2)单例模式

超常见,这里以线程安全的单例模式为例:

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

 3)代理模式

 代理:类似专业的中介,代替我们去做某些事。

 

public interface Sourceable {  
    public void method();  
}  

public class Source implements Sourceable {  
  
    @Override  
    public void method() {  
        System.out.println("the original method!");  
    }  
}  

public class Proxy implements Sourceable {  
  
    private Source source;  
    public Proxy(){  
        super();  
        this.source = new Source();  
    }  
    @Override  
    public void method() {  
        before();  
        source.method();  
        atfer();  
    }  
    private void atfer() {  
        System.out.println("after proxy!");  
    }  
    private void before() {  
        System.out.println("before proxy!");  
    }  
} 

 测试类:

public class ProxyTest {  
  
    public static void main(String[] args) {  
        Sourceable source = new Proxy();  
        source.method();  
    }  
  
}  

 结果:

before proxy!
the original method!
after proxy!

代理模式的应用场景:

如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:

1、修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。

2、就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。

使用代理模式,可以将功能划分的更加清晰,有助于后期维护!

 

4)桥接模式

把事物和其具体实现分开,使他们可以各自独立的变化。桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBC桥DriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。

 

public interface Sourceable {  
    public void method();  
}  
//分别定义两个实现类:


public class SourceSub1 implements Sourceable {  
  
    @Override  
    public void method() {  
        System.out.println("this is the first sub!");  
    }  
}  

public class SourceSub2 implements Sourceable {  
  
    @Override  
    public void method() {  
        System.out.println("this is the second sub!");  
    }  
}  
//定义一个桥,持有Sourceable的一个实例:


public abstract class Bridge {  
    private Sourceable source;  
  
    public void method(){  
        source.method();  
    }  
      
    public Sourceable getSource() {  
        return source;  
    }  
  
    public void setSource(Sourceable source) {  
        this.source = source;  
    }  
}  

public class MyBridge extends Bridge {  
    public void method(){  
        getSource().method();  
    }  
}  

 测试类:

 

public class BridgeTest {  
      
    public static void main(String[] args) {  
          
        Bridge bridge = new MyBridge();  
          
        /*调用第一个对象*/  
        Sourceable source1 = new SourceSub1();  
        bridge.setSource(source1);  
        bridge.method();  
          
        /*调用第二个对象*/  
        Sourceable source2 = new SourceSub2();  
        bridge.setSource(source2);  
        bridge.method();  
    }  
}  

 打印结果:

this is the first sub!
this is the second sub!

 

5)享元模式(参考数据库连接池)

 享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用

 

6)策略模式

策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。

策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。

public interface MemberStrategy {
    /**
     * 计算图书的价格
     * @param booksPrice    图书的原价
     * @return    计算出打折后的价格
     */
    public double calcPrice(double booksPrice);
}

  //初级会员折扣类


public class PrimaryMemberStrategy implements MemberStrategy {

    @Override
    public double calcPrice(double booksPrice) {
        
        System.out.println("对于初级会员的没有折扣");
        return booksPrice;
    }

}

  //中级会员折扣类


public class IntermediateMemberStrategy implements MemberStrategy {

    @Override
    public double calcPrice(double booksPrice) {

        System.out.println("对于中级会员的折扣为10%");
        return booksPrice * 0.9;
    }

}

  //高级会员折扣类


public class AdvancedMemberStrategy implements MemberStrategy {

    @Override
    public double calcPrice(double booksPrice) {
        
        System.out.println("对于高级会员的折扣为20%");
        return booksPrice * 0.8;
    }
}

   //价格类


public class Price {
    //持有一个具体的策略对象
    private MemberStrategy strategy;
    /**
     * 构造函数,传入一个具体的策略对象
     * @param strategy    具体的策略对象
     */
    public Price(MemberStrategy strategy){
        this.strategy = strategy;
    }
    
    /**
     * 计算图书的价格
     * @param booksPrice    图书的原价
     * @return    计算出打折后的价格
     */
    public double quote(double booksPrice){
        return this.strategy.calcPrice(booksPrice);
    }
}

 测试类:

public class StrategyTest{

    public static void main(String[] args) {
        //选择并创建需要使用的策略对象
        MemberStrategy strategy = new AdvancedMemberStrategy();
        //创建环境
        Price price = new Price(strategy);
        //计算价格
        double quote = price.quote(300);
        System.out.println("图书的最终价格为:" + quote);
    }

}

 打印结果:

对于高级会员的折扣为20%

 

图书的最终价格为:240.0

 

………………

策略模式基本上答了这些就够了~