整理一下对应的java基础的面试题
JDK(Java Development Kit),Java开发工具包
JRE(Java Runtime Environment),Java运行环境
JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。
简单来说就是JDK是Java的开发工具,JRE是Java程序运行所需的环境,JVM是Java虚拟机.它们之间的关系是JDK包含JRE和JVM,JRE包含JVM.
Java是一种面向对象的语言
Java通过Java虚拟机实现了平台无关性,一次编译,到处运行
支持多线程
支持网络编程
具有较高的安全性和可靠性
Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
Java 支持自动垃圾回收,而 C++ 需要手动回收。
Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操 作符重载,而 C++ 可以。
Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
对于基本类型,= = 比较的是值;
对于引用类型,= = 比较的是地址;
equals不能用于基本类型的比较;
如果没有重写equals,equals就相当于==;
如果重写了equals方法,equals比较的是对象的内容;
final修饰的成员变量,必须在声明的同时赋值,一旦创建不可修改;
final修饰的方法,不能被子类重写;
final类中的方法默认是final的;
private类型的方法默认是final的;
Math提供了三个与取整有关的方法:ceil、floor、round
1、ceil:向上取整;
Math.ceil(11.3) = 12;
Math.ceil(-11.3) = 11;
2、floor:向下取整;
Math.floor(11.3) = 11;
Math.floor(-11.3) = -12;
3、round:四舍五入;
加0.5然后向下取整。
Math.round(11.3) = 11;
Math.round(11.8) = 12;
Math.round(-11.3) = -11;
Math.round(-11.8) = -12;
不属于。
八种基本数据类型:byte、short、char、int、long、double、float、boolean。
在 Java 8 中,String 内部使用 char 数组存储数据。并且被声明为final,因此它不可被继承。
为什么String要设计成不可变的呢(不可变性的好处):
1.可以缓存 hash 值()
因为 String 的hash值经常被使用,例如String 用做 HashMap 的 key。不可变的特性可以使得 hash值也不可变, 因此只需要进行一次计算。
2.常量池优化
String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
3.线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地
使用。
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小 字符常量占两个字节 字符串常量占若干个字节(至少一个字符结束标志)
String str="i"会将起分配到常量池中,常量池中没有重复的元素,如果常量池中存中i,就将i的地址赋给变量,如果没有就创建一个再赋给变量。
String str=new String(“i”)会将对象分配到堆中,即使内存一样,还是会重新创建一个新的对象。
将对象封装到stringBuilder中,调用reverse方法反转。
1、常见String类的获取功能
length:获取字符串长度;
charAt(int index):获取指定索引位置的字符;
indexOf(int ch):返回指定字符在此字符串中第一次出现处的索引;
substring(int start):从指定位置开始截取字符串,默认到末尾;
substring(int start,int end):从指定位置开始到指定位置结束截取字符串;
2、常见String类的判断功能
equals(Object obj): 比较字符串的内容是否相同,区分大小写;
contains(String str): 判断字符串中是否包含传递进来的字符串;
startsWith(String str): 判断字符串是否以传递进来的字符串开头;
endsWith(String str): 判断字符串是否以传递进来的字符串结尾;
isEmpty(): 判断字符串的内容是否为空串"";
3、常见String类的转换功能
byte[] getBytes(): 把字符串转换为字节数组;
char[] toCharArray(): 把字符串转换为字符数组;
String valueOf(char[] chs): 把字符数组转成字符串。valueOf可以将任意类型转为字符串;
toLowerCase(): 把字符串转成小写;
toUpperCase(): 把字符串转成大写;
concat(String str): 把字符串拼接;
4、常见String类的其他常用功能
replace(char old,char new) 将指定字符进行互换
replace(String old,String new) 将指定字符串进行互换
trim() 去除两端空格
int compareTo(String str) 会对照ASCII 码表 从第一个字母进行减法运算 返回的就是这个减法的结果,如果前面几个字母一样会根据两个字符串的长度进行减法运算返回的就是这个减法的结果,如果连个字符串一摸一样 返回的就是0。
这些方法都很重要,面试经常会问到,要结合其他知识将这些方法理解透彻
Object clone():创建与该对象的类相同的新对象
boolean equals(Object):比较两对象是否相等
void finalize():当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
Class getClass():返回一个对象运行时的实例类
int hashCode():返回该对象的散列码值
void notify():唤醒等待在该对象的监视器上的一个线程
void notifyAll():唤醒等待在该对象的监视器上的全部线程
String toString():返回该对象的字符串表示
void wait():在其他线程调用此对象的 notify() 方法或 notifyAll()方法前,导致当前线程等待
对象1:new StringBuilder()
对象2:new String(“a”)
对象3:常量池中的"a"
对象4:new String(“b”)
对象5:常量池中的"b"
深入剖析:StringBuilder中的toString():
对象6:new String(“ab”)
强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
附加题:
String s1 = new String("1") + new String("1");//s1变量记录的地址为:new String
s1.intern();//在字符串常量池中生成"11"。如何理解:jdk6:创建了一个新的对象"11",也就有新的地址;jdk7:此时常量池中并没有创建"11",而是创建了一个指向堆空间中new String("11")的地址;
String s2 = "11";
System.out.println(s1 == s2);//jdk6:false;jdk7:true
抽象类不能被实例化;
抽象类可以有抽象方法,只需申明,无须实现;
有抽象方法的类一定是抽象类;
抽象类的子类必须实现抽象类中的所有抽象方法,否则子类仍然是抽象类;
抽象方法不能声明为静态、不能被static、final修饰。
1、接口
接口使用interface修饰;
接口不能实例化;
类可以实现多个接口;
①java8之前,接口中的方法都是抽象方法,省略了public abstract。②java8之后;接口中可以定义静态方法,静态方法必须有方法体,普通方法没有方法体,需要被实现;
2、抽象类
抽象类使用abstract修饰;
抽象类不能被实例化;
抽象类只能单继承;
抽象类中可以包含抽象方法和非抽象方法,非抽象方法需要有方法体;
如果一个类继承了抽象类,①如果实现了所有的抽象方法,子类可以不是抽象类;②如果没有实现所有的抽象方法,子类仍然是抽象类。
1、按流划分,可以分为输入流和输出流;
2、按单位划分,可以分为字节流和字符流;
字节流:inputStream、outputStream;
字符流:reader、writer;
1、同步阻塞BIO
一个连接一个线程。
JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。
2、同步非阻塞NIO
NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。
JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。
一个请求一个线程。
3、异步非阻塞AIO
一个有效请求一个线程。
JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。
exist
createFile
createDirectory
write
read
copy
size
delete
move
所谓反射,是java在运行时进行自我观察的能力,通过class、constructor、field、method四个方法获取一个类的各个组成部分。
在Java运行时环境中,对任意一个类,可以知道类有哪些属性和方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于反射机制。
优点:运行期类型的判断,动态加载类,提高代码灵活度。
缺点:性能比直接的java代码要慢很多。
Java的很多框架都用到了反射,例如Spring中的xml的配置模式等
动态代理设计模式也采用了反射机制
序列化就是一种用来处理对象流的机制。将对象的内容流化,将流化后的对象传输于网络之间。
序列化是通过实现serializable接口,该接口没有需要实现的方法,implement Serializable只是为了标注该对象是可被序列化的,使用一个输出流(FileOutputStream)来构造一个ObjectOutputStream对象,接着使用ObjectOutputStream对象的writeObejct(Object object)方法就可以将参数的obj对象到磁盘,需要恢复的时候使用输入流。
序列化是将对象转换为容易传输的格式的过程。
例如,可以序列化一个对象,然后通过HTTP通过Internet在客户端和服务器之间传输该对象。在另一端,反序列化将从流中心构造成对象。
一般程序在运行时,产生对象,这些对象随着程序的停止而消失,但我们想将某些对象保存下来,这时,我们就可以通过序列化将对象保存在磁盘,需要使用的时候通过反序列化获取到。
对象序列化的最主要目的就是传递和保存对象,保存对象的完整性和可传递性。
譬如通过网络传输或者把一个对象保存成本地一个文件的时候,需要使用序列化。
序列化:将对象写入到IO流中
反序列化:从IO流中恢复对象
序列化的意义:将Java对象转换成字节序列,这些字节序列更加便于通过网络传输或存储在磁盘上,在需要时可以通过反序列化恢复成原来的对象。
实现方式:
实现Serializable接口
实现Externalizable接口
序列化的注意事项:
对象的类名、实例变量会被序列化;方法、类变量、transient实例变量都不会被序列化。
某个变量不被序列化,可以使用transient修饰。
序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
反序列化时必须有序列化对象的class文件。
1、什么要使用克隆?
想对一个对象进行复制,又想保留原有的对象进行接下来的操作,这个时候就需要克隆了。
2、如何实现对象克隆?
实现Cloneable接口,重写clone方法;
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。
BeanUtils,apache和Spring都提供了bean工具,只是这都是浅克隆。
3、深拷贝和浅拷贝区别是什么?
浅拷贝:仅仅克隆基本类型变量,不克隆引用类型变量;
深克隆:既克隆基本类型变量,又克隆引用类型变量;
深拷贝的实现方式
1.重载clone方法
public class Main{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, CloneNotSupportedException {
Address s = new Address("天津");
Person p = new Person("张三", 23, s);
System.out.println("克隆前的地址:" + p.getAddress().getName());
Person cloneP = (Person) p.clone();
cloneP.getAddress().setName("北京");
System.out.println("克隆后的地址:" + cloneP.getAddress().getName());
}
}
class Address implements Cloneable {
private String city;
public Address(String name){
this.city = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getName() {
return city;
}
public void setName(String name) {
this.city = name;
}
}
class Person implements Cloneable{
private String name;
private int age;
private Address address;
public Person(String name, int age, Address address){
this.name = name;
this.age = age;
this.address = address;
}
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.address = (Address)address.clone();
return person;
}
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 Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
输出
克隆前的地址:天津
克隆后的地址:北京
其实就是Person类和Address类都要重写clone方法,这里面需要注意的一点是super.clone()为浅克隆,所以在在Person类中重写clone方法时,address对象需要调用address.clone()重新赋值,因为address类型为引用类型。
2.序列化
public class Main{
public static void main(String[] args) throws IOException, ClassNotFoundException {
Address s = new Address("天津");
Person p = new Person("张三", 23, s);
System.out.println("克隆前的地址:" + p.getAddress().getName());
Person cloneP = (Person) p.deepClone();
cloneP.getAddress().setName("北京");
System.out.println("克隆后的地址:" + cloneP.getAddress().getName());
}
}
class Address implements Serializable{
private String city;
public Address(String name){
this.city = name;
}
public String getName() {
return city;
}
public void setName(String name) {
this.city = name;
}
}
class Person implements Serializable{
private String name;
private int age;
private Address address;
public Person(String name, int age, Address address){
this.name = name;
this.age = age;
this.address = address;
}
public Object deepClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
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 Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
输出
克隆前的地址:天津
克隆后的地址:北京
1、throw
作用在方法内,表示抛出具体异常,由方法体内的语句处理;
一定抛出了异常;
2、throws
作用在方法的声明上,表示抛出异常,由调用者来进行异常处理;
可能出现异常,不一定会发生异常;
final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写
final关键字主要用于修饰类,变量,方法。
类:被final修饰的类不可以被继承
方法:被final修饰的方法不可以被重写
变量:被final修饰的变量是基本类型,变量的数值不能改变;被修饰的变量是引用类型,变量便不能在引用其他对象,但是变量所引用的对象本身是可以改变的。
finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
finalize方法用于垃圾回收。
一般情况下不需要我们实现finalize,当对象被回收的时候需要释放一些资源,比如socket链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize方法,关闭这个链接。
但是当调用finalize方法后,并不意味着gc会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题。所以,不推荐使用finalize方法。
catch–>finally–>catch 中return
public class Main{
public static void main(String[] args) {
int a = test1();
System.out.println(a);
int b = test2();
System.out.println(b);
int c = test3();
System.out.println(c);
int d = test4();
System.out.println(d);
int e = test5();
System.out.println(e);
}
public static int test1(){
int a = 1;
try{
a = 2;
return a;
}catch(Exception e){
System.out.println("hello,test1");
a = 3;
}finally{
a = 4;
}
return a;
}
//输出 2
public static int test2(){
int a = 1;
try{
a = 2;
return a;
}catch(Exception e){
System.out.println("hello,test2");
a = 3;
return a;
}finally{
a = 4;
}
}
//输出 2
public static int test3(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
System.out.println("hello,test3");
a = 3;
}finally{
a = 4;
}
return a;
}
//输出 hello,test3
// 4
public static int test4(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
System.out.println("hello,test4");
a = 3;
return a;
}finally{
a = 4;
}
}
//输出 hello,test4
// 3
public static int test5(){
int a = 1;
try{
a = 2/0;
return a;
}catch(Exception e){
a = 3;
return a;
}finally{
a = 4;
return a;
}
}
//输出 4
}
如果没有仔细的研究过,应该好多会猜错,下面总结下规律。
从前三个例子可以看出如果try{}中的代码没有异常,catch(){}代码块中的代码不会执行。所以如果try{}和catch(){}都含有return时,无异常执行try{}中的return,存在异常执行catch(){}的return。
不管任何情况,就算try{}或catch(){}中含有return,finally{}中的代码一定会执行,那么为什么test1、test2、test3中的结果不是4呢,因为虽然finally是在return后面的表达式运算之后执行的,但此时并没有返回运算之后的值,而是把值保存起来,不管finally对该值做任何的改变,返回的值都不会改变,依然返回保存起来的值。也就是说方法的返回值是在finally运算之前就确定了的。
如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。这个不理解可以看看上面提到的Java的值传递的问题。
如果finally{}中含有return,会导致程序提前退出,不在执行try{}或catch(){}中的return。所以test5返回的值是4。
最后总计一下try{}catch(){}finally{}的执行顺序。
先执行try中的语句,包括return后面的表达式;
有异常时,执行catch中的语句,包括return后面的表达式,无异常跳过catch语句;
然后执行finally中的语句,如果finally里面有return语句,执行return语句,程序结束;
finally{}中没有return时,无异常执行try中的return,如果有异常时则执行catch中的return。前两步执行的return只是确定返回的值,程序并未结束,finally{}执行之后,最后将前两步确定的return的返回值返回。
(1)String
String是不可变对象,每次对String类型的改变时都会生成一个新的对象。
(2)StringBuilder
线程不安全,效率高,多用于单线程。
(3)StringBuffer
线程安全,由于加锁的原因,效率不如StringBuilder,多用于多线程。
不频繁的字符串操作使用String,操作频繁的情况不建议使用String。
StringBuilder > StringBuffer > String。
Java中Object有一个方法:
public native int hashcode();
(1)hashcode()方法的作用
hashcode()方法主要配合基于散列的集合一起使用,比如HashSet、HashMap、HashTable。
当集合需要添加新的对象时,先调用这个对象的hashcode()方法,得到对应的hashcode值,实际上hashmap中会有一个table保存已经存进去的对象的hashcode值,如果table中没有改hashcode值,则直接存入,如果有,就调用equals方法与新元素进行比较,相同就不存了,不同就存入。
(2)equals和hashcode的关系
如果equals为true,hashcode一定相等;
如果equals为false,hashcode不一定不相等;
如果hashcode值相等,equals不一定相等;
如果hashcode值不等,equals一定不等;
(3)重写equals方法时,一定要重写hashcode方法
静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。
(1)重载是多态的集中体现,在类中,要以统一的方式处理不同类型数据的时候,可以用重载。
(2)重写的使用是建立在继承关系上的,子类在继承父类的基础上,增加新的功能,可以用重写。
(3)简单总结:
重载是多样性,重写是增强剂;
目的是提高程序的多样性和健壮性,以适配不同场景使用时,使用重载进行扩展;
目的是在不修改原方法及源代码的基础上对方法进行扩展或增强时,使用重写;
总结:方法重写时,参数列表,返回值得类型是一定不能修改的,异常可以减少或者删除,但是不能抛出新的异常或者更广的异常,方法的访问权限可以降低限制,但是不能做更严格的限制。
(4)在里氏替换原则中,子类对父类的方法尽量不要重写和重载。(我们可以采用final的手段强制来遵循)
隐式(自动)类型转换:从存储范围小的类型到存储范围大的类型。byte→short(char)→int→long→float→double
显示(强制)类型转换:从存储范围大的类型到存储范围小的类型。double→float→long→int→short(char)→byte。该类类型转换很可能存在精度的损失。
看一个经典的代码
short s = 1;
s = s + 1;
这是会报错的,因为1是int型,s+1会自动转换为int型,将int型直接赋值给short型会报错。
做一下修改即可避免报错
short s = 1;
s = (short)(s + 1);
或这样写,因为s += 1会自动进行强制类型转换
short s = 1;
s += 1;
Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的。
具体可以查看valueof方法中的是否是大于某个值才会新建对象
Double、Float的valueOf方法的实现是类似的。
直接新建对象
然后是Boolean的valueOf方法是单独一组的。
静态对象
Integer i = new Integer(xxx)和Integer i =xxx的区别,这两者的区别主要是第一种会触发自动装箱,第二者不会,最后看看下面这段程序的输出结果
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Long g = 3L;
int int1 = 12;
int int2 = 12;
Integer integer1 = new Integer(12);
Integer integer2 = new Integer(12);
Integer integer3 = new Integer(1);
System.out.println("c==(a+b) ->"+ (c==(a+b)));
System.out.println("g==(a+b) ->" + (g==(a+b)));
System.out.println( "c.equals(a+b) ->" + (c.equals(a+b)));
System.out.println( "g.equals(a+b) ->" + (g.equals(a+b)));
System.out.println("int1 == int2 -> " + (int1 == int2));
System.out.println("int1 == integer1 -> " + (int1 == integer1));
System.out.println("integer1 == integer2 -> " + (integer1 == integer2));
System.out.println("integer3 == a1 -> " + (integer3 == a));
}
}
结果
c==(a+b) ->true
g==(a+b) ->true
c.equals(a+b) ->true
g.equals(a+b) ->false
int1 == int2 -> true
int1 == integer1 -> true
integer1 == integer2 -> false
integer3 == a1 -> false
下面简单解释这些结果。
1.当 "= ="运算符的两个操作数都是包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。所以c= =a+b,g==a+b为true。
2.而对于equals方法会先触发自动拆箱过程,再触发自动装箱过程。也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较。所以c.equals(a+b)为true。而对于g.equals(a+b),a+b会先拆箱进行相加运算,在装箱进行equals比较,但是装箱后为Integer,g为Long,所以g.equals(a+b)为false。
3.int1 = = int2为true无需解释,int1 = = integer1,在进行比较时,integer1会先进行一个拆箱操作变成int型在进行比较,所以int1 == integer1为true。
4.integer1 = = integer2->false。integer1和integer2都是通过new关键字创建的,可以看成两个对象,所以integer1 = = integer2 为false。integer3 = = a1 -> false , integer3是一个对象类型,而a1是一个常量它们存放内存的位置不一样,所以integer3 == a1为false,具体原因可学习下java的内存模型。
switch可以作用于char byte short int及它们对应的包装类型,switch不可作用于long double float boolean及他们的包装类型。在 JDK1.5之后可以作用于枚举类型,在JDK1.7之后可作用于String类型。
Java语言采用Unicode编码标准,它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。
在Java编程语言中有四种权限访问控制符,这四种访问权限的控制符能够控制类中成员的可见性。其中类有两种public、default。而方法和变量有 4 种:public、default、protected、private。
public : 对所有类可见。使用对象:类、接口、变量、方法
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
default : 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
&&和&
&&和&都可以表示逻辑与,但他们是有区别的,共同点是他们两边的条件都成立的时候最终结果才是true;不同点是&&只要是第一个条件不成立为false,就不会再去判断第二个条件,最终结果直接为false,而&判断的是所有的条件。
||和|
||和|都表示逻辑或,共同点是只要两个判断条件其中有一个成立最终的结果就是true,区别是||只要满足第一个条件,后面的条件就不再判断,而|要对所有的条件进行判断。
static关键字的主要用途就是方便在没有创建对象时调用方法和变量和优化程序性能
1.static变量(静态变量)
用static修饰的变量被称为静态变量,也被称为类变量,可以直接通过类名来访问它。静态变量被所有的对象共享,在内存中只有一个副本,仅当在类初次加载时会被初始化,而非静态变量在创建对象的时候被初始化,并且存在多个副本,各个对象拥有的副本互不影响。
2.static方法(静态方法)
static方法不依赖于任何对象就可以进行访问,在static方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用,但是在非静态成员方法中是可以访问静态成员方法/变量的。
public class Main {
public static String s1 = "s1";//静态变量
String s2 = "s2";
public void fun1(){
System.out.println(s1);
System.out.println(s2);
}
public static void fun2(){
System.out.println(s1);
System.out.println(s2);//此处报错,静态方法不能调用非静态变量
}
}
3.static代码块(静态代码块)
静态代码块的主要用途是可以用来优化程序的性能,因为它只会在类加载时加载一次,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。如果程序中有多个static块,在类初次被加载的时候,会按照static块的顺序来执行每个static块。
public class Main {
static {
System.out.println("hello,word");
}
public static void main(String[] args) {
Main m = new Main();
}
}
4.可以通过this访问静态成员变量吗?(可以)
this代表当前对象,可以访问静态变量,而静态方法中是不能访问非静态变量,也不能使用this引用。
5.初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。如果存在继承关系的话,初始化顺序为父类中的静态变量和静态代码块——子类中的静态变量和静态代码块——父类中的实例变量和普通代码块——父类的构造函数——子类的实例变量和普通代码块——子类的构造函数.
重点掌握前三种即可
1.this关键字可用来引用当前类的实例变量。主要用于形参与成员名字重名,用this来区分。
public Person(String name, int age) {
this.name = name;
this.age = age;
}
2.this关键字可用于调用当前类方法。
public class Main {
public void fun1(){
System.out.println("hello,word");
}
public void fun2(){
this.fun1();//this可省略
}
public static void main(String[] args) {
Main m = new Main();
m.fun2();
}
}
3.this()可以用来调用当前类的构造函数。(注意:this()一定要放在构造函数的第一行,否则编译不通过)
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this(name);
this.age = age;
}
}
4.this关键字可作为调用方法中的参数传递。
5.this关键字可作为参数在构造函数调用中传递。
6.this关键字可用于从方法返回当前类的实例。
1.super可以用来引用直接父类的实例变量。和this类似,主要用于区分父类和子类中相同的字段
2.super可以用来调用直接父类构造函数。(注意:super()一定要放在构造函数的第一行,否则编译不通过)
3.super可以用来调用直接父类方法。
public class Main {
public static void main(String[] args) {
Child child = new Child("Father","Child");
child.test();
}
}
class Father{
protected String name;
public Father(String name) {
this.name = name;
}
public void Say(){
System.out.println("hello,child");
}
}
class Child extends Father{
private String name;
public Child(String name1, String name2) {
super(name1); //调用直接父类构造函数
this.name = name2;
}
public void test(){
System.out.println(this.name);
System.out.println(super.name); //引用直接父类的实例变量
super.Say(); //调用直接父类方法
}
}
相同点:
super()和this()都必须在构造函数的第一行进行调用,否则就是错误的
this()和super()都指的是对象,所以,均不可以在static环境中使用。
不同点:
super()主要是对父类构造函数的调用,this()是对重载构造函数的调用
super()主要是在继承了父类的子类的构造函数中使用,是在不同类中的使用;this()主要是在同一类的不同构造函数中的使用
break结束当前的循环体
continue结束本次循环,进入下一次循环
return结束当前方法
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
封装就是隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
多态是同一个行为具有多个不同表现形式或形态的能力。这句话不是很好理解,可以看这个解释,在Java语言中,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中实现多态的三个必要条件:继承、重写、向上转型。继承和重写很好理解,向上转型是指在多态中需要将子类的引用赋给父类对象。
public class Main {
public static void main(String[] args) {
Person person = new Student(); //向上转型
person.run();
}
}
class Person {
public void run() {
System.out.println("Person");
}
}
class Student extends Person { //继承
@Override
public void run() { //重载
System.out.println("Student");
}
}
运行结果为
Student
一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
使用多个小的专门的接口,而不要使用一个大的总接口。
Java程序存在继承,在执行子类的构造方法时,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。如果父类只定义了有参数的构造函数,而子类的构造函数没有用super调用父类那个特定的构造函数,就会出错。
帮助子类做初始化工作。
主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
方法名称和类同名
不用定义返回值类型
不可以写retrun语句
构造方法可以被重载
类变量:独立于方法之外的变量,用static修饰。
实例变量:独立于方法之外的变量,不过没有 static 修饰。
局部变量:类的方法中的变量。
成员变量:成员变量又称全局变量,可分为类变量和实例变量,有static修饰为类变量,没有static修饰为实例变量。
内部类包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类
1.成员内部类定义为位于另一个类的内部,成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
class Outer{
private double a = 0;
public static int b =1;
public Outer(double a) {
this.a = a;
}
class Inner { //内部类
public void fun() {
System.out.println(a);
System.out.println(b);
}
}
}
2.当成员内部类拥有和外部类同名的成员变量或者方法时,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:外部类.this.成员变量
3.在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。
4.成员内部类是依附外部类而存在的,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。
匿名内部类只没有名字的内部类,在日常开发中使用较多。使用匿名内部类的前提条件:必须继承一个父类或实现一个接口。
interface Person {
public void fun();
}
class Demo {
public static void main(String[] args) {
new Person() {
public void fun() {
System.out.println("hello,word");
}
}.fun();
}
}
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
class Outter {
int a = 1;
static int b = 2;
public Outter() {
}
static class Inner {
public Inner() {
System.out.println(a);//报错,静态内部类不能访问非静态变量
System.out.println(b);
}
}
}
public class Main{
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
1.内部类不为同一包的其他类所见,具有很好的封装性;
2.匿名内部类可以很方便的定义回调。
3.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
4.内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
public class Main {
public static void main(String[] args) {
}
public void fun(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
对于变量a可以从生命周期的角度理解,局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁,而局部内部类对局部变量的引用依然存在,如果局部内部类要调用没有final修饰的局部变量时,就会造成生命周期不一致出错。
对于变量b,其实是将fun方法中的变量b以参数的形式对匿名内部类中的拷贝(变量b的拷贝)进行赋值初始化。在run方法中访问的变量b根本就不是test方法中的局部变量b,而是一个拷贝值,所以不存在生命周期不一致的问题,但如果在run方法中修改变量b的值会导致数据不一致,所以需要加final修饰。
构造器可以被重载,不能被重写
不能,因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。
这是一个很容易搞混又很难解释清楚的问题,先说结论,Java中只有值传递
先看这样一段代码
public class Main{
public static void main(String[] args) {
int a = 1;
printValue(a);
System.out.println("a:" + a);
}
public static void printValue(int b){
b = 2;
System.out.println("b:"+ b);
}
}
输出
b:2
a:1
可以看到将a的值传到printValue方法中,并将其值改为2。但方法调用结束后,a的值还是1,并未发生改变,所以这种情况下为值传递。
再看这段代码
public class Main{
public static void main(String[] args) {
Preson p = new Preson();
p.name = "zhangsan";
printValue(p);
System.out.println("p.name: " + p.name);
}
public static void printValue(Preson q){
q.name = "lisi";
System.out.println("q.name: "+ q.name);
}
}
class Preson{
public String name;
}
输出结果
q.name: lisi
p.name: lisi
在将p传入printValue方法后,方法调用结束,p的name属性竟然被改变了!所以得出结论,参数为基本类型为值传递,参数为引用类型为时为引用传递。这个结论是错误的,下面来看看判断是值传递还是值传递的关键是什么,先看定义
值传递:是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递:是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
从定义中可以明显看出,区分是值传递还是引用传递主要是看向方法中传递的是实际参数的副本还是实际参数的地址。上面第一个例子很明显是值传递,其实第二个例子中向printValue方法中传递的是一个引用的副本,只是这个副本引用和原始的引用指向的同一个对象,所以副本引用修改过对象属性后,通过原始引用查看对象属性肯定也是被修改过的。换句话说,printValue方法中修改的是副本引用指向的对象的属性,不是引用本身,如果修改的是引用本身,那么原始引用肯定不受影响。看下面这个例子
public class Main{
public static void main(String[] args) {
Preson p = new Preson();
p.name = "zhangsan";
printValue(p);
System.out.println("p.name: " + p.name);
}
public static void printValue(Preson q){
q = new Preson();
q.name = "lisi";
System.out.println("q.name: "+ q.name);
}
}
class Preson{
public String name;
}
输出结果
q.name: lisi
p.name: zhangsan
可以看到将p传入printValue方法后,printValue方法调用结束后,p的属性name没有改变,这是因为在printValue方法中并没有改变副本引用q所指向的对象,而是改变了副本引用q本身,将副本引用q指向了另一个对象并对这个对象的属性进行修改,所以原始引用p所指向的对象不受影响。所以证明Java中只存在值传递。
NullPointerException:空指针异常;
SQLException:数据库相关的异常;
IndexOutOfBoundsException:数组下角标越界异常;
FileNotFoundException:打开文件失败时抛出;
IOException:当发生某种IO异常时抛出;
ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出此异常;
NoSuchMethodException:无法找到某一方法时,抛出;
ArrayStoreException:试图将错误类型的对象存储到一个对象数组时抛出的异常;
NumberFormatException:当试图将字符串转换成数字时,失败了,抛出;
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数。
ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
异常主要分为Error和Exception两种
Error:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理。
EXception:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
非检查异常(unckecked exception):该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。因为这样的异常发生的原因很可能是代码写的有问题。
检查异常(checked exception):除了Error和 RuntimeException的其它异常。这是编译器要求必须处理的异常。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,所以必须处理这些异常。
面试问的不多,但是在使用框架开发时会经常使用,但东西太多了,这里只是简单介绍下概念。
Annotation注解可以看成是java中的一种标记记号,用来给java中的类,成员,方法,参数等任何程序元素添加一些额外的说明信息,同时不改变程序语义。注解可以分为三类:基本注解,元注解,自定义注解
标准注解
@Deprecated:该注解用来说明程序中的某个元素(类,方法,成员变量等)已经不再使用,如果使用的话的编译器会给出警告。
@SuppressWarnings(value=“”):用来抑制各种可能出现的警告。
@Override:用来说明子类方法覆盖了父类的方法,保护覆盖方法的正确使用
元注解(元注解也称为元数据注解,是对注解进行标注的注解,元注解更像是一种对注解的规范说明,用来对定义的注解进行行为的限定。例如说明注解的生存周期,注解的作用范围等)
@Target(value=“ ”):该注解是用来限制注解的使用范围的,即该注解可以用于哪些程序元素。
@Retention(value=“ ”):用于说明注解的生存周期
@Documnent:用来说明指定被修饰的注解可以被javadoc.exe工具提取进入文档中,所有使用了该注解进行标注的类在生成API文档时都在包含该注解的说明。
@Inherited:用来说明使用了该注解的父类,其子类会自动继承该注解。
@Repeatable:java1.8新出的元注解,如果需要在给程序元素使用相同类型的注解,则需将该注解标注上。
Java 泛型是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型擦除(这是面试考察泛型时经常问到的问题)
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。看下面代码
public class Main{
public static void main(String[] args) {
ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>();
System.out.println(arrayList1.getClass() == arrayList2.getClass());
}
}
输出结果
true
可以看到ArrayList和ArrayList的原始类型是相同,在编译成字节码文件后都会变成List,JVM看到的只有List,看不到泛型信息,这就是泛型的类型擦除。在看下面这段代码
public class Main{
public static void main(String[] args) throws Exception {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "a");
System.out.println(arrayList.get(0));
System.out.println(arrayList.get(1));
}
}
输出
1
a
可以看到通过反射进行add操作,ArrayList竟然可以存储字符串,这是因为在反射就是在运行期调用的add方法,在运行期泛型信息已经被擦除。
既然存在类型擦除,那么Java是如何保证在ArrayList添加字符串会报错呢?
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。