面试系列(四):Java基础知识
基础也是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
………………
策略模式基本上答了这些就够了~