java面试题
创建对象的五种方法
1、使用new关键字
Test test = new Test();
2、通过反射
- 使用class类的newInstance方法
Test test = Test.class.newInstance();
//声明要抛出的异常对象IllegalAccessException, InstantiationException
- 使用Constructor.newInstance方法
Test test = Test.class.getConstructor().newInstance();
//声明要抛出异常对象NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException
3、使用clone方法
Test test = new Test();
Test test1 = test.clone();
//声明要抛出throws CloneNotSupportedException
4、使用反序列化
当序列化和反序列化一个对象,jvm会创建一个单独的对象
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("data.obj"));
Test test = (Test) inputStream.readObject();
//声明要抛出IOException, ClassNotFoundException
面向对象的三大特征
万物皆可归类,类是对于世界事物的高度抽象,不同的事物有不同的关系:一个类自身与外界的封装关系;一个父类与子类的继承关系;一个类和多个类的多态关系。
万物皆对象,对象是具体的世界事物,面向对象的三大特征:封装、继承、多态,封装说明一个行为和属性与其他的类的关系,继承是父类和子类的关系,多态是类与类之前的关系
封装
概念
封装就是把抽象的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作,才能对数据进行操作
理解和好处
1、封装隐藏了实现细节,类的内部机制实现不会呈现,细节是写方法的人考虑的,而外部只需要传入必要的参数实现操作就可以了
2、封装可以对数据进行验证,保证合理性
Person p = new Person();
p.name = "jack";
p.age = 1000;
显而易见,不合理的地方是年龄,不封装时可以对数据进行修改,并且系统并不会发现其不合理,而封装就是在方法内部对数据的验证和处理
3、可以在不影响使用的情况下改变类的内部结构,同时保护了内部数据。一个类内部中,通常会对其属性进行私有化private,所以不能直接修改属性,而提供了公有的set方法,可以对数据进行判断和处理,而属性私有化后,外部无法访问,又提供了get方法,用于对数据进行获取
继承
概念
继承是从已有类中派生出新的类,新的类能吸收已有类的数据属性和方法,并且扩展新的能力,不用再重新写已有类的属性和方法,直接继承即可。通过extends关键字来实现继承
好处
1、继承避免了对于一般类和特殊类之间的共同特征进行的重复的特征描写,通过继承可以清晰得表达每一项共同特征所适应的概念范围,在一般类中定义的属性和方法适用于这个类本身以及这个类以下的每一层特殊类的全部对象。通过继承原则使得系统模型比较简练和清晰
对于小学生、中学生和大学生共有的属性,姓名、年龄、性别等属性可以写在一个父类中,而小学生类使用时直接通过extends来获取,不需要再重写
//student类
public class Student {
public String name;
public int age;
public void setAge(int age) {
this.age = age;
}
public void show(){
System.out.println("学生名:"+name+"年龄:"+age);
}
}
//子类pupil
public class Pupil extends Student{
public void testing(){
System.out.println("年龄为"+age+"的小学生"+name+"正在考试...");
}
}
//测试
public class Test {
public static void main(String[] args) {
Pupil pupil = new Pupil();
pupil.name = "rose";
pupil.age = 19;
pupil.testing();//年龄为19的小学生rose正在考试...
}
}
2、扩展性和维护性提高了
当在父类中添加一个方法或者属性,而其下的所有类都可以调用这个方法或者属性
细节
1、父类中使用private 定义的变量和方法不会被继承,不能在子类中直接操作父类定义私有的变量和方法,但是可以在父类中公有的方法来获取属性和方法。
//父类中属性私有,子类不能调用
private String name;//属性私有
private void call(){
}//方法私有
public String showName(){
return name;
}
public void showCall(){
call();
}
2、子类在创建对象必须调用父类的构造器,完成父类的初始化
//父类
public Student(){
System.out.println("父类构造器被调用....");
}
//子类
public Pupil(){
System.out.println("子类构造器被调用....");
}
//测试
Pupil pupil = new Student();
输出:父类构造器被调用....
子类构造器被调用....
默认在子类的构造器中有super()
,无论写与不写都默认存在
3、当创建子类对象时,不管使用子类的哪个构造器,默认情况下都会调用父类的无参构造器;如果父类没有提供无参构造器,则必须在子类的构造器中用super去指向使用父类的哪一个构造器完成对父类的初始化,否则编译不会通过
4、如果希望指定去调用父类的某一个构造器,就需要显式的调用一下
super(参数列表)
注意:
- super在使用时,必须放在构造器的第一行,构造器在执行时,先执行父类的构造器后执行子类的构造器
- super()和this()都只能放在构造器第一行,因此这两个方法不能共同存在一个构造器中国
5、java所有类都是Object的子类,Object是所有类的父类
6、父类构造器的调用不限于直接父类,将一直往上追溯直到Object类,也就是他们的顶级父类
7、子类最多只能直接继承一个父类,java中是单继承机制
8、不能滥用继承,子类和父类之间必须满足is-a的逻辑关系
即Cat is Animal、Pupil is Student
继承本质
分析子类继承父类,创建子类对象时,内存发生了什么?
案例:
public class ExtendsTheory {
public static void main(String[] args) {
Son son = new Son();
}
}
class Grandpa{
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends Grandpa{
String name = "大头爸爸";
int age = 39;
}
class Son extends Father{
String name = "大头儿子";
}
内存布局:
1、先加载顶级父类object,然后再加载grandpa,再加载father,最后加载son类
2、在堆存放son对象的数据,然后指向方法区的字符串常量池
3、当把堆的数据分配好后,并且指向也建立好后,0x11就会配给主方法对象引用
而创建完Son对象后,如何调用Grandpa和Father类中相同的属性
- 首先看子类是否有该属性,如果子类有这个属性,并且可以访问(可能属性私有化),则返回子类属性的信息
- 如果子类没有该属性,就向上查找父类有没有这个属性,如果父类有属性,并且可以访问,就返回父类属性的信息
- 如果父类没有这个属性,继续向上找父类,直到Obejct,还是没有,就会报错
注意
- 一个类只能继承一个抽象类,但是可以实现多个接口
- 一个接口可以继承多个接口,类是单继承的,接口是多继承的
多态
当类很多,而且对对象操作操作的方法很多,不利于管理和维护,从而多态就被建立了
方法或对象具有多种形态是面向对象的第三大特征,多态是建立在封装和继承基础上的
多态的具体体现
方法的多态,重写和重载就体现了多态
重载
public int sum(int n1,int n2){ return n1+n2; } public int sum(int n1,int n2,int n3){ reruen n1+n2+n3; }
两个方法构成重载,通过不同的参数个数去调用sum方法,就会去调用不同的方法,对于sum方法来说就是多种状态的体现
重写
A类中方法 public void show{ System.out.println("A类方法show被调用了"); } 在B类中重写show方法 public void show{ System.out.println("B类方法show被调用了"); }
在一个不同对象中写相同方法就是重写,对于show方法来说就是多种状态的体现
对象的多态
- 一个对象的编译类型和运行类型可以不一样
- 编译类型在定义对象时,就确定了不能改变
- 运行类型是可以变化的
- 编译类型看定义时
=
号的左边,运行类型看=
号的右边
Animal animal = new Dog(); //animal的编译类型是Animal,而运行类型是Dog animal = new Cat(); //animal的运行类型变成了Cat, 而编译类型仍然是animal animal.call();//输出的是Cat的call方法 //运行时,执行到这行时,animal的运行类型是Cat,所以是Cat的call()
多态可以统一管理对象操作方法
public void feed(Animal animal,Food food){
System.out.println("主人"+name+"给"+animal.getName()+"吃"+food.getName ());
}
animal编译类型是Animal,可以指向Animal子类的对象,而food编译类型是Food,可以指向Food子类的对象,当animal子类对象执行方法,不用在对方法进行重写或重载,只需要运行对象初始化成使用对象即可调用方法,不用再具体编译
细节分析
1、首先确定的是,多态是封装和继承的基础之上建立的,多态的前提是两个对象存在继承关系
2、还有就是多态的向上转型,他的本质就是父类的引用指向了子类的对象(animal指向cat)
- 可以调用父类的所有成员,但是还是遵守访问权限
- 不能调用子类特有的成员
- 最终的运行结果看子类的具体实现,即调用方法从运行类型的本类开始查找方法
3、多态的向下转型
Dog dog = (Dog) animal();
子类类型 引用名 = (子类类型)父类引用
这时dog的编译类型和运行类型都是Dog
- 只能强转父类的引用,不能强转父类的对象
- 要求父类的引用必须指向的是当前的目标类型的对象(dog指向dog)
- 可以调用子类类型中所有的成员
注意:
Dog dog = (Dog) animal();
Cat cat = (Cat) animal();
//会报错
这时Animal向下转型成dog,指向了Dog对象,而下一句就是cat指向现在父类animal的引用指向Dog对象,即cat指向Dog对象,明显是不对的
ArrayList和LinkedList的区别
相同
ArrayList和LinkedList都实现了List接口
不同
底层结构
1、ArrayList的底层结构是数组,以O(1)时间复杂度对元素进行随机访问
2、LinkedList的底层结构是双向链表,每一个节点存储了两个引用,一个指向前一个元素,一个指向后一个元素。在这种情况下查找某个元素较快
增删的效率
1、ArrayList添加元素时依靠数组扩容进行添加,删除元素时需要重排数组中的所有数据,都是效率较低
无参构造时初始elementData数组容量为0,第一次添加,则扩容elementData为10,如果需要再次扩容,则扩容elemenData1.5倍;如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍
2、LinkedList添加元素通过链表追加进行添加,效率较高
添加一个新的节点,通过索引连接前一个节点和后一个节点
改查的效率
1、ArrayList是基于索引的数据接口,使用索引在数组中搜索和修改数据的速度很快。
2、LinkedList每一个节点存储了两个引用,一个指向前一个元素,一个指向后一个元素,查找元素只能从首元素开始查询,依次获得下一个元素的地址对其值进行查询,效率较慢
内存占用
1、ArrayList添加元素时依靠数组扩容进行添加,数组长度不够时在进行扩容添加,内存占用较低
2、LinkedList需要较大的内存,每一个节点存储的是数值和前后节点的位置
一个LinkedList实例存储了两个值: Node < E > first 和Node< E > last分别
表示链表的其实节点和尾节点,每个Node实例存储了三个值:
(E item,Node next,Node pre)。
如何选择
1、如果改查的操作较多,选择ArrayList
2、如果增删的操作较多,选择LinkedList
3、一般来说,在程序中大多数都是查询,因此大部分情况下都会选择ArrayList
4、在一个项目中,根据业务灵活变化,可能一个模块使用的是ArrayList,另一个模块使用的是LinkedList,要根据业务来选择
底层结构 | 增删的效率 | 改查的效率 | 内存占用 | |
---|---|---|---|---|
ArrayList | 数组 | 使用数组扩容,较低 | 较高 | 较低 |
LinkedList | 双向链表 | 使用链表,较高 | 较低 | 较多 |
java锁
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发的可能性低,每次去拿数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断在此期间别人有没有去更新这个数据。在写时先读取当前版本号 ,再进行加锁操作(比较上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写
的操作
乐观锁通过CAS操作实现的,是一种更新的原子操作,比较当前值和传入值是否一样,一样则更新,否则失败
悲观锁
悲观锁是一种悲观思想,认为写比读多,遇到并发的可能性高,每次去拿数据时认为别人会修改,所以每次都会上锁。别人想要读取的话这个数据就会block直到拿到锁
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
自旋锁
如果持有锁的线程在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要在内核态和用户态之间的切换进入阻塞挂起状态,进入自旋状态,等持有锁的线程释放算后立即获取锁,这样就能避免用户线程和内核的切换的消耗
线程自旋是需要消耗 cpu 的,说白了就是让 cpu 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cpu 自旋做无用功,所以需要设定一个自旋等待的最大时间
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态
JRE和JDK的区别
JDK
JDK是Java Development Kit
,是功能齐全的java SDK,拥有JRE所有的一切,还有编译器 (javac)和工具(jdb)。能够创建程序和编译程序
JRE
JRE是java运行时环境,是运行已编译java程序所需的所有内容的集合,包括虚拟机(JVM),java类库,java命令和其他的基础构件,但是不能创建新程序
区别
JRE主要包含:Java类库的class文件(在lib目录下打包成了jar)和虚拟机(jvm.dll)
JDK主要包含:Java类库的class文件(在lib目录下打包成了jar)并自带一个JRE,而且jdk/jre/bin下的client 和server两个文件夹下都包含jvm.dll(说明JDK自带的JRE有两个虚拟机)。
JDK为什么要自带一个JRE
java是提供给更多的人使用,是不需要做开发的,只需要JRE能够让java程序跑起来就行了,而在平时java文件运行前,需要配置环境变量,没有javac和java是运行不了的,而且jdk/bin目录下包含了所有的命令,在这种情况下每个客户还需要手动配置环境变量,所以在安装jre时自动吧jre的java.exe加到了系统变量中
java异常处理方式
java通过面向对象的方式进行异常处理,一旦方法出现异常,系统自动根据该异常对象寻找合适的异常处理器(Exception Handler)来处理异常,把各种不同的异常进行分类,并提供了良好的接口。
java中,每个异常都是一个对象,它是Throwable类或其他子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。
java的异常处理是通过5个关键词来实现的,throw
、try
、catch
、finally
、throws
案例:
int num1 = 12;
int num2 = 0;
int res = num1/num2;
System.out.println(res);
众所周知,除数是不能为0的,这个在程序也是严格规定的,所以他会出现数学运算异常,导致运行不了
因此程序远认为一端代码可能会出现异常,可以使用try-catch来抛出异常
try {
res = num1/num2;
} catch (Exception e) {
e.printStackTrace();
}
虽然还是会抛出异常,但是还是输出了结果
基本介绍
1、Java语言中,将程序执行中发生的不正常情况称为异常,但是开发过程中的语法错误和逻辑错误不是异常
2、执行过程中所发生的异常事件分为两大类
- Error:java虚拟机无法解决的严重错误,jvm系统内部错误、资源耗尽,比如:栈溢出(StackOverflowError)和OOM,error是严重错误,程序会崩溃
- Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。比如空指针异常,连接超时等
异常处理类型
- 编译异常的特点:编译时就需要捕获该异常,否则报编译错误,称为受检异常
- SQLException:操作数据库时,查询表可能发生异常
- IOException:操作文件时,发生的异常
- FileNotFoundException:当操作一个不存在的文件时,发生异常
- ClassNotException:加载类,该类不存在时,发生异常
- EOFException:操作文件到文件末尾,发生异常
- IllegalArguementException:参数异常
- 运行异常的特点:可以不捕获异常,也不报编译错误,称为非受检异常
Java源程序用javac.exe
编译成字节码文件过程中可能出现编译异常,而运行异常是在字节码文件用java.exe在内存中加载、运行类可能出现的异常就是运行异常,运行时异常,编译器不要求强制处理异常,因为这类异常很普遍,全处理的话会对程序的可读性和运行效率产生影响;编译异常是编译器必须处置的异常
异常处理方式
五个关键字
try-catch-finally
try{ //将可能有异常的代码放入 }catch(Exception e){ //捕获到异常 //当异常发生时,系统将异常封装成Exception对象e,传递给catch //得到异常对象后,程序员自己处理 }finally{ //不管异常是否发生,都会执行的操作。一般是释放资源的代码 }
try-catch
- 如果异常发生了,则异常发生后面的代码不会执行,直接进入到catch里面
- 如果异常没有发生,则顺序执行try的代码模块,不会进入到catch
- 如果希望异常不论是否发生,都执行某段代码,就使用finally代码模块
try-finally
- 这种用法相当于没有捕获异常,因此程序会直接崩掉
- 应用场景执行一段代码,无论是否发生异常,都必须执行某个业务逻辑
throws异常处理
1、如果一个方法可能生成某种异常,但是不能确定如果处理这种异常,则该方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由方法的调用者负责处理,而方法的调用者也可以选择继续处理或者向上抛出
2、在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是他的父类
public void f1() throws FileNotFoundException{
FileInputStream file = new FileInputStream("d://a.text");
//没有这个文件,会抛出异常,直接方法后面throws直接抛出这个异常类型,可以抛出多个或者直接抛出父类Exception
}
注意:
对于编译异常,程序中必须处理,比如用try-catch或者throws都可以
对于运行异常,程序中没有处理,默认的处理方法就是Throws
子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出的异常类型和父类抛出的异常一致或者父类抛出异常类型的子类型
当在方法中调用另一个方法,这个方法会抛出异常,而抛出异常的类型决定是否报错
public static void f1() throws FileNotFoundException{ FileInputStream file = new FileInputStream("d://a.text"); } public static void f2(){ f1(); //会报错 //FileNotFoundException属于编译异常,在执行f2方法时,f1会抛出异常,而f2没有异常的解决方法 }
public static void f1() throws ArithmeticException{ } public static void f2(){ f1(); //不会报错 //因为ArithmeticException属于运行异常,并不要求程序员显示处理,因为运行异常有默认的处理方法Throws }
自定义异常
1、定义:自定义异常类名继承Exception或RuntimeException
2、如果继承Exception,属于编译异常
3、如果继承RuntimeException,属于运行异常
public class CustomException {
public static void main(String[] args) {
int age = 80;
if(!(age>=18 && age <=120)){
throw new AgeException("年龄需要在18—120之间");
}
System.out.println("你的年龄正确!!");
}
}
class AgeException extends RuntimeException{
public AgeException(String message){
super(message);
}
}
throw和throws的区别
1、位置不同
throw:方法内部
throws:方法的签名处
2、内容不同
throw+异常对象,抛出某个异常对象
throws+异常的类型
3、作用不同
throw:异常出现的源头,手动制造异常
throws:在方法的声明处,告诉方法的调用者,这个可能会出现声明的异常。
意义 | 位置 | 后面跟的东西 | |
---|---|---|---|
throws | 异常处理的方式 | 方法声明处 | 异常类型 |
throw | 手动生成异常对象的关键字 | 方法体中 | 异常对象 |
String、StringBuffer、StringBuilder区别及使用场景
java提供了两种类型的字符串:String和StringBuffer/StringBuilder,都可以存储和操作字符串
String
基本介绍
1、String对象用于保存字符串
2、双引号括起的字符序列,就是字符串常量
3、字符串的字符使用了Unicode字符编码,一个字符占两个字节
4、String类有很多构造器,实现了构造器的重载
- String s1 = new String(String original)
- String s2 = new String(char[] a)
- String s3 = new String(char[] a,int startIndex,int count)
- String s4 = new String(byte[] b)
5、String实现了Serializable接口,说明String可以串行化,可以在网络上进行传输
6、实现了Comparable接口,说明String对象可以比较
7、String是一个final类,代表不可变的字符序列并且不能被其他的类继承
8、String定义了一个私有的不被继承的char类型的数组value,用于存放字符串内容,不可以修改
不可以再指向另外一个对象,但是单个字符内容是变化的
创建方式
1、直接赋值
String s = "baibai";
先从常量池查看是否有“baibai”数据空间,如果有,直接指向;如果没有则重新创建,然后指向。s最终指向的是常量池的空间地址
2、调用构造器
String s = new String("baibai");
先在堆中创建空间,里面维护了value属性,指向常量池中的baibai空间。如果常量池中没有“baibai”,则重新创建,如果没有,直接通过value指向。s最终指向的是堆中的空间地址
特性
1、String是一个final类,代表不可变的字符序列
String s = "baibai";
s = "huihui";
//创建了一个“huihui”的字符串常量对象,让s改变指向,指向“huihui”字符串常量对象
String a = "hello"+"abc";
//创建了一个“helloabc”的字符串常量对象,+是将拼接起来创建的
String a = "hello";
String b = "abc";
String c = a+b;
/**
* 1.先创建一个StringBuilder sb = new StringBuilder();
* 2.执行sb.append("hello");
* 3.在此基础上继续sb.append("abc");
* 4.String c = sb.toString();
* 最后c指向堆中的对象(String)类型的value[]->池中"helloabc"
*/
案例:
String效率低的原因
string s = "a";
s += "b";
实际上原来的”a”字符串对象已经丢弃了,现在又产生了一个字符串s+ “b” (也就是“ ab”)。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大
影响程序的性能得出结论:如果我们对String做大量修改,不要使用String
StringBuffer
基本介绍
1、StringBuffer代表可变的字符序列,可以对字符串内容可以进行增删
2、StringBuffer是可变长度的
3、StringBuffer的直接父类是AbstractStringBuilder
4、StringBuffer实现了serializable,即StringBuffer的对象可以串行化
5、在父类中,AbstractStringBuilder的属性char[] value,不是final。该 value数组存放字符串内容,引出存放在堆中的
6、StringBuffer是final类,不能被继承
String和StringBuffer的区别
String保存的是字符串常量,里面的值不能更改,每次String类的更新实际上就是更改地址(每次创建新的对象),效率较低
StringBuffer保存的是字符串常量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容,不用更新后地址,效率较高
构造器
- StringBuffer():构造一个其中不带字符的字符串缓冲区,其初始容量为16个字符串
- StringBuffer(CharSequence seq):构造一个字符串缓冲区,包含与指定CharSequence相同的字符
- StringBuffer(int capacity):构造一个不带字符,但是具有初始容量额字符串缓冲区。即对char[] 大小进行指定
- StringBuffer(String str):构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容,长度为当前字符串长度加上16
String和StringBuffer的相互转换
1、String–>StringBuffer的方法
构造器
String str = "hello"; StringBuffer stringBuffer = new StringBuffer(str); //返回的是StringBuffer对象,但是对str没有影响
append
StringBuffer stringBuffer1 = new StringBuffer(); stringBuffer1 = stringBuffer1.append(str);
2、StringBuffer–>String
toString
StringBuffer stringBuffer2 = new StringBuffer("数据结构"); String s = stringBuffer2.toString();
构造器
String s1 = new String(stringBuffer2);
注意
1、当字符串为null,使用StringBuffer的append方法添加,调用的是底层的AbstractStringBuilder的appendNull方法,因此而当他输出字符串的长度为4
2、当使用构造器创建StringBuffer的对象时,输出对象,会抛出空指针异常
StringBuilder
基本介绍
1、一个可变的字符序列,提供了一个与StringBuffer兼容的API,但不保证同步。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先采用该类,因为在大多实现中,比StringBufffer要快
2、StringBuffer和StringBuilder均代表可变的字符序列,方法是一样的
3、StringBuilder的方法没有做互斥的处理,没有用synchronized关键字,因此在单线程的情况下使用
区别
1、String是一个final类,代表不可变的字符序列,也就是String引用的字符串是不能改变的
2、StringBuffer/StringBuilder表示的字符串对象可以直接进行修改,而且方法也一样
3、StringBuilder是java5中引入的,和StringBuffer的方法完全相同。区别在与它是单线程环境下使用的,因为他的所有方法都没有synchronized修饰,他的效率理论上比StringBuffer要高
类 | 字符序列类型 | 效率 | 线程是否安全 |
---|---|---|---|
String | 不可变字符序列 | 效率低,但是复用率高 | |
StringBuffer | 可变字符序列 | 效率较高(在增删情况下) | 安全 |
StringBuilder | 可变字符序列 | 效率最高 | 不安全 |
接口和抽象类的区别
1、抽象类有构造方法,用于子类实例化使用;接口没有构造方法
2、抽象类中的成员变量可以是常量,也可以是变量;接口中的成员变量只能是常量,就是用public static final
修饰的
3、抽象类必须在类前用abstract关键字修饰;而接口的方法会被隐式指定为public abstract
方法,用其他的关键字会报错
4、一个类只能继承一个抽象类,而却可以实现多个接口
5、抽象类不用全部实现接口中所有的方法,其余方法的实现可以交给子类来实现;接口中的方法被抽象类实现的话,可以不去实现接口里面的抽象方法,也可以被抽象类的子类实现;而被非抽象类实现的话,必须实现接口中的所有抽象方法
两个byte类型的数据相加会报错
byte b1=1,b2=2,b3,b6;
final byte b4=4,b5=6;
b6=b4+b5;
b3=(b1+b2);//报错,提示int类型转化成byte类型可能会导致精度的缺失
System.out.println(b3+b6);
原因:由于java的运算机制导致的,默认的整数类型是int类型,浮点类型是double类型,进行byte运算时先将byte提升为int类型后进行计算,结果类型就是int类型,如果没有强制类型转换。所以两个byte类型相加的结果是int类型,等于byte类型,因此编译就会报错
运算机制
- 两个byte类型相加,会自动转换成int类型,结果也是int类型
- 两个short类型相加,会自动转换成int类型,结果也是int类型
- 两个char类型相加,会自动转换成int类型,结果也是int类型(两个字符的ascll码相加)
- 两个float类型相加,会自动转换成double类型,结果也是double类型
- 两个Long类型相加,不会转换成int类型,Long比int的数据范围大的多,结果类型还是Long类型
byte,short,int,long,float,double,char,boolean
注意:这里的运算指的都是变量,如果常量运算超出数据范围,也会报错
jsp的内置对象
- request对象:客户端的请求信息封装在request对象中,通过他了解客户端的需求,然后做出响应。是HttpServletRequest类的实例
- response对象:包含了响应客户请求的信息,是HttpServletResponse对象的实例
- session对象:客户端与服务器的一次会话,从客户连到服务器的一个WebApplication开始,直到客户端与服务端断开连接为止
- out对象:是jspWriter类的实例,向客户端输出内容常用的对象
- page对象:当前jsp页面本身,是java.long.Object类的实例
- application对象:实现了用户间数据的共享,可存放全局变量。开始于服务器的启动,直到服务器的关闭,是ServletContext类的实例
- exception对象:当页面运行出现错误,就产生这个对象。jsp页面要应用此对象,需要将isErrorPage设为true
- pageContext对象,提供了对jsp页面所有的对象的访问
- config对象:在一个Servlet初始化时,jsp向他传递信息的,包括初始化时所要用到的参数以及服务器的有关信息
构造方法的特点
- 构造方法的方法名必须与类名相同
- 构造方法没有返回类型,也不能定义为void,在方法名前面不声明方法类型
- 主要作用是完成对象的初始化工作,将定义对象时的参数传给对象域
- 一个类可以定义多个构造方法,没有构造方法的话,编译系统会自动插入一个无参数的默认构造器,不执行任何代码
- 构造方法可以重载,以参数的个数,类型,顺序
访问权限
修饰符 | 类内部 | 同一个包 | 子类 | 任何地方 |
---|---|---|---|---|
private | √ | |||
default | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
静态块、main()、构造块、构造方法执行顺序
public class Test {
public static Test t1 = new Test();
public static Test t2 = new Test();
{
System.out.println("构造块");
}
static {
System.out.println("静态块");
}
public static void main(String[] args) {
Test t = new Test();
}
}
- 输出:构造块 构造块 静态块 构造块
静态块:用static申明,JVM加载类时执行,仅执行一次
构造块:类中直接用{}定义,每一次创建对象时执行
执行顺序优先级:静态块>main()>构造块>构造方法
静态块按照申明顺序执行,所以先执行public static Test t1 = new Test();该语句创建对象,则又会调用构造块,输出构造块
接着执行public static Test t1 = new Test();输出构造块
再执行
static
{
System.out.println("静态块");
}输出静态块
最后main方法执行,创建对象,输出构造块。
Java内存模型中的可见性、原子性和有序性
1、可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到
- volatile修饰的变量,具有可见性,同时不允许线程内部缓存和重排序,直接修改内存,所以对其他线程是可见的;但是不能保证具有可见性。比如volatile int a=0;a++,这个变量a具有可见性,但是这个a++操作是一个非原子操作,同时这个操作存在线程安全问题
2、原子是世界的最小单位,具有不可分割性。非原子操作都会存在线程安全问题,需要同步技术来操作,称为他具有原子性。在Java中synchronized和在lock、unlock中保证原子性
3、有序性
- volatile是因为本身禁止指令重排序
- synchronized是由一个变量只允许一个线程对其进行lock操作
volatile关键字
- volatile用来保证变量的更新操作通知到其他线程。当把变量声明为volatile类型中,编译器与运行都会注意到这个变量是共享的,不会将此变量上的操作和其他内存操作一起重排序。
- volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
- 在访问volatile变量时不会执行加锁操作,因此也就不会执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量化级的同步机制
内部类的分类
内部类分为静态内部类
,成员内部类
,局部内部类
,匿名内部类
静态内部类
- 定义在类内部的静态类,就是静态内部类
public class Outer{
private static int radius = 1;
static class StaticScanner{
public void visit(){
System.out.println("visit outer static variable:" + radius);
}
}
}
- 静态内部类可以访问外部类所有的静态变量,而不能访问外部类的非静态变量;创建方式:
new 外部类.静态内部类
成员内部类
- 定义在类内部,成员位置上的非静态类,就是内部类
public class Outer{
private static int radius = 1;
private int count = 2;
class Inner{
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}
- 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有;创建方式:
内部类实例.new 内部类()
局部内部类
- 定义在方法中的内部类,就是局部内部类
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 编译错误,定义在静态方法中的局部
类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}
- 定义在实例方法中的局部类可以访问外部类所有的变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法;创建方式:在对应方法内,new 内部类()
匿名内部类
- 没有名字的内部类
public class Outer {
private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
//匿名内部类必须继承或实现一个已有的接口
interface Service{
void method();
}
- 特点
- 匿名内部类必须继承一个抽象类或者实现一个接口
- 匿名内部类不能定义任何静态成员和方法
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为final
- 匿名内部类不能是抽象的,必须实现继承的类或实现的接口的所有抽象方法
反射机制
什么是反射机制
在运行状态中,对于任意一个类,都能知道这个类所有的属性和方法;对于任意一个对象,都能调用它任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制
静态编译和动态编译
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
应用场景举例
1、使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序
2、Spring通过xml配置模式装配Bean的过程:
- 将程序中所有的xml或properties配置文件加载入内存中
- java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制根据这个字符串获取某个类的class实例
- 动态配置实例的属性
获取反射的三种方法
1.通过new对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制
public class Student{
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get{
public static void main(String[] args) throws ClassNotFoundException{
//通过建立对象
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//通过相对路径
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//通过类名
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}