面向对象是一种思想,可以将复杂问题简单化,让我们从执行者变为了指挥者。
封装主要是为了增加程序的可读性,解耦合并且隐藏部分实现细节。
public void setAge(int age) {
if (age < 0 || age > 60)
throw new RuntimeException("年龄设置不合法");
this.age = age;
}
通过将Student这个类的name和age属性私有化,只有通过公共的get/set方法才能进行访问,在get/set方法中我们可以对内部逻辑进行封装处理,外部的调用方不必关心我们的处理逻辑。
Java中不支持多继承,即一个类只可以有一个父类存在。另外Java中的构造函数是不可以继承的,如果构造函数被private修饰(单例模式),那么就是不明确的构造函数,该类是不可以被其它类继承的,具体原因我们可以先来看下Java中类的初始化顺序:
Java 程序在执行子类的构造方法之前,如果没有用 super()
来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
注意:构造方法没有返回值,但不能用 void 声明构造函数
如果父类构造函数是私有(private)的,则初始化子类的时候不可以被执行,所以解释了为什么该类不可以被继承,也就是说其不允许有子类存在。我们知道,子类是由其父类派生产生的,那么子类有哪些特点呢?
覆盖也叫重写,是指子类和父类之间方法的一种关系,比如说父类拥有方法A,子类扩展了方法A并且添加了丰富的功能。那么我们就说子类覆盖或者重写了方法A,也就是说子类中的方法与父类中继承的方法有完全相同的返回值类型、方法名、参数个数以及参数类型。
Java 中有三个访问权限修饰符:private
、protected
以及 public
,如果不加访问修饰符,表示包级可见。
可以对类或类中的成员(字段以及方法)加上访问修饰符。
protected
用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
通过方法的覆盖和重载可以实现多态。
多态主要通过三种方式来实现:
重载是指在一个类中(包括父类)存在多个同名的不同方法,这些方法的参数个数,顺序以及类型不同均可以构成方法的重载。如果仅仅是修饰符、返回值、抛出的异常不同,那么这是2个相同的方法。
public class OverLoadTest {
public void method1(String name, int age){
System.out.println("");
}
// 两个方法的参数顺序不同,可以构成方法的重载
public void method1(int age, String name){
System.out.println("");
}
//---------------------------------------------
public void method2(String name){
System.out.println("");
}
// 两个方法的参数类型不同,可以构成方法的重载
public void method2(int age){
System.out.println("");
}
//---------------------------------------------
public void method3(String name){
System.out.println("");
}
// 两个方法的参数个数不同,可以构成方法的重载
public void method3(int age, int num){
System.out.println("");
}
}
只有方法返回值不同,可以构成重载吗?
调用某个方法,有时候并不关心其返回值,这个时候编译器根据方法名和参数无法确定我们调用的是哪个方法。
将子类对象作为父类对象使用来实现多态:
把不同的子类对象都当作父类对象来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。这样操作之后,父类的对象就可以根据当前赋值给它的子类对象的特性以不同的方式运作。
对象的引用型变量具有多态性,因为一个引用型变量可以指向不同形式的对象,即:子类的对象作为父类的对象来使用。在这里涉及到了向上转型和向下转型,我们分别介绍如下:
向上转型:
子类对象转为父类,父类可以是接口。
公式:Father f = new Son()
; Father是父类或接口,Son是子类。
向下转型:
父类对象转为子类。公式:Son s = (Son) f
;
在向上转型的时候我们可以直接转,但是在向下转型的时候我们必须强制类型转换。并且,如案例中所述,该父类必须实际指向了一个子类对象才可强制类型向下转型,即其是以这种方式Father f = new Son()
创建的父类对象。若以Father f = new Father()
这种方式创建的父类对象,那么不可以转换向下转换为子类的Son对象,运行会报错,因为其本质还是一个Father对象。
介绍:
优点
缺点
JDK(Java Development Kit)
是一个开发工具包,是Java开发环境的核心组件,并且提供编译、调试和运行一个Java程序所需要的所有工具,可执行文件和二进制文件,是一个平台特定的软件JRE(Java Runtime Environment)
是指Java运行时环境,是JVM的实现,提供了运行Java程序的平台。JRE包含了JVM,但是不包含Java编译器/调试器之类的开发工具JVM(Java Virtual Machine)
是指Java虚拟机,当我们运行一个程序时,JVM负责将字节码转换为特定机器代码,JVM提供了内存管理/垃圾回收和安全机制等区别与联系:
所以,Java是一种先编译,后解释执行的语言。
public
,其抽象方法为了被重写所以不能使用 private
关键字修饰!static final
类型的,必须被初始化,接口中只有常量,没有变量抽象类和接口应该如何选择?分别在什么情况下使用呢?
JDK8接口中的方法默认实现:
public interface MyInterface {
// 定义一个已经实现的方法,使用default表明
default void say(String message){
System.out.println("Hello "+message);
}
// 普通的抽象方法
void test();
}
如果两个接口中存在同样的默认方法:
public interface MyInterface {
// 定义一个已经实现的方法,使用default表明
default void say(String message){
System.out.println("Hello "+message);
}
// 普通的抽象方法
void test();
}
interface MyInterface2{
// 定义一个已经实现的方法,使用default表明
default void say(String message){
System.out.println("[2]-Hello "+message);
}
}
// 此处会编译错误, 两个接口中都有say方法
class MyClass implements MyInterface, MyInterface2{
@Override
public void test() {
System.out.println("test...");
}
}
编译错误解决方案:
class MyClass implements MyInterface, MyInterface2{
@Override
public void say(String message) {
System.out.println("[Client]-Hello "+message);
}
@Override
public void test() {
System.out.println("test...");
}
}
class MyClass implements MyInterface, MyInterface2{
// 手动指定哪个默认方法生效
public void say(String message) {
MyInterface.super.say(message);
}
@Override
public void test() {
System.out.println("test...");
}
}
JDK8中为什么会出现默认方法呢?
使用接口,使得我们可以面向抽象编程,但是其有一个缺点就是当接口中有改动的时候,需要修改所有的实现类。在JDK8中,为了给已经存在的接口增加新的方法并且不影响已有的实现,所以引入了接口中的默认方法实现。
默认方法允许在不打破现有继承体系的基础上改进接口,解决了接口的修改与现有的实现不兼容的问题。该特性在官方库中的应用是:给java.util.Collection
接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()
等等。在我们实际开发中,接口的默认方法应该谨慎使用,因为在复杂的继承体系中,默认方法可能引起歧义和编译错误。
参考文章:
JAVA中限制接口流量、并发的方法
JAVA中限制接口流量可以通过Guava的RateLimiter
类或者JDK自带的Semaphore
类来实现,两者有点类似,但是也有区别,要根据实际情况使用。简单来说,
一、RateLimiter类
RateLimiter翻译过来是速率限制器,使用的是一种叫令牌桶的算法,当线程拿到桶中的令牌时,才可以执行。通过设置每秒生成的令牌数来控制速率。使用例子如下:
public class TestRateLimiter implements Runnable {
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static final RateLimiter limiter = RateLimiter.create(1); // 允许每秒最多1个任务
public static void main(String[] arg) {
for (int i = 0; i < 10; i++) {
limiter.acquire(); // 请求令牌,超过许可会被阻塞
Thread t = new Thread(new TestRateLimiter());
t.start();
}
}
public void run() {
System.out.println(sdf.format(new Date()) + " Task End..");
}
}
二、Semaphore类
Semaphore翻译过来是信号量,通过设置信号量总数,当线程拿到信号量,才可以执行,当执行完毕再释放信号量。从而控制接口的并发数量。使用例子如下:
初始化中的第二个参数true代表以公平的方式获取信号量,即先进先出的原则。
public class TestSemaphore implements Runnable {
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static final Semaphore semaphore = new Semaphore(5, true); // 允许并发的任务量限制为5个
public static void main(String[] arg) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new TestSemaphore());
t.start();
}
}
public void run() {
try {
semaphore.acquire(); // 获取信号量,不足会阻塞
System.out.println(sdf.format(new Date()) + " Task Start..");
Thread.sleep(5000);
System.out.println(sdf.format(new Date()) + " Task End..");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放信号量
}
}
}
Java 不能隐式执行向下转型,因为这会使得精度降低。
1.1
字面量属于 double
类型,不能直接将 1.1
直接赋值给 float
变量,因为这是向下转型。
// float f = 1.1;
1.1f
字面量才是 float
类型。
float f = 1.1f;
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
short s1 = 1;
// s1 = s1 + 1; 不能隐式地将 int 类型下转型为 short 类型
但是使用 += 或者 ++ 运算符可以执行隐式类型转换。
s1 += 1;
// s1++;
上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
s1 = (short) (s1 + 1);
Java提供了两个类型系统,基本类型与引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:
基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“。
装箱:从基本类型转换为对应的包装类对象。
构造方法:
Integer(int value)
构造一个新分配的 Integer 对象,它表示指定的 int 值。Integer(String s)
构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。传递的字符串,必须是基本类型的字符串,否则会抛出异常 "100" 正确 "a" 抛异常
。静态方法:
static Integer valueOf(int i)
返回一个表示指定的 int 值的 Integer 实例。static Integer valueOf(String s)
返回保存指定的 String 的值的 Integer 对象。拆箱:从包装类对象转换为对应的基本类型。
成员方法:
int intValue()
以 int 类型返回该 Integer 的值。//装箱:把基本类型的数据,包装到包装类中(基本类型的数据->包装类)
//构造方法
Integer in1 = new Integer(1); // 方法上有横线,说明方法过时了
System.out.println(in1);//1 重写了toString方法
Integer in2 = new Integer("1");
System.out.println(in2);//1
//静态方法
Integer in3 = Integer.valueOf(1);
System.out.println(in3);
//Integer in4 = Integer.valueOf("a");//NumberFormatException数字格式化异常
Integer in4 = Integer.valueOf("1");
System.out.println(in4);
//拆箱:在包装类中取出基本类型的数据(包装类->基本类型的数据)
int i = in1.intValue();
System.out.println(i);
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
编译器会在自动装箱过程调用 Integer.valueOf()
方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:
import java.util.ArrayList;
/*
自动装箱与自动拆箱:基本类型的数据和包装类之间可以自动的相互转换
JDK1.5之后出现的新特性
*/
public class Demo {
public static void main(String[] args) {
/*
自动装箱:直接把int类型的整数赋值包装类
Integer in = 1; 就相当于 Integer in = new Integer(1);
*/
Integer in = 1; //自动装箱
/*
自动拆箱:in是包装类,无法直接参与运算,可以自动转换为基本数据类型,在进行计算
in+2;就相当于 in.intVale() + 2 = 3
in = in.intVale() + 2 = 3 又是一个自动装箱
*/
in = in+2; // 拆箱
ArrayList<Integer> list = new ArrayList<>();
/*
ArrayList集合无法直接存储整数,可以存储Integer包装类
list是Integer类型列表,放入1有自动装箱过程。
*/
list.add(1); //-->自动装箱 list.add(new Integer(1));
int a = list.get(0); //-->自动拆箱 list.get(0).intValue();
}
}
基本类型转换为String:
//基本类型->字符串(String)
//方法一
int i1 = 100;
String s1 = i1+""; // -> 字符串
System.out.println(s1+200);//100200
//方法二
String s2 = Integer.toString(100);
System.out.println(s2+200);//100200
//方法三
String s3 = String.valueOf(100);
System.out.println(s3+200);//100200
String转换成对应的基本类型:
除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型:
public static byte parseByte(String s)
:将字符串参数转换为对应的byte基本类型。public static short parseShort(String s)
:将字符串参数转换为对应的short基本类型。public static int parseInt(String s)
:将字符串参数转换为对应的int基本类型。public static long parseLong(String s)
:将字符串参数转换为对应的long基本类型。public static float parseFloat(String s)
:将字符串参数转换为对应的float基本类型。public static double parseDouble(String s)
:将字符串参数转换为对应的double基本类型。public static boolean parseBoolean(String s)
:将字符串参数转换为对应的boolean基本类型。注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException
异常。
//字符串(String)->基本类型
int i = Integer.parseInt(s1);
System.out.println(i-10);
不是数字类型的字符串,转成数字会报错。
int a = Integer.parseInt("a"); //NumberFormatException 不是数字类型的字符串
System.out.println(a);
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注
解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
Java中提供了4个元注解,元注解的作用是负责注解其它注解。分别是@Target,@Retention、@Documented、@Inherited
@Target
:
说明注解所修饰的对象范围,关键源码如下:
public @interface Target {
ElementType[] value();
}
public enum ElementType {
TYPE,FIELD,METHOD,PARAMETED,CONSTRUCTOR,LOCAL_VARIABLE,ANNOCATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE
}
例如,如下的注解使用@Target标注,表明MyAnn注解就只能作用在类/接口、方法、成员变量上。
@Target({ElementType.TYPE, ElementType.METHOD,ElementType.FIELD})
public @interface MyAnn {
}
@Rentention
:(保留策略)
保留策略定义了该注解被保留的时间长短。关键源码如下:
public @interface Retention {
RetentionPolicy value();
}
public enum RetentionPolicy {
SOURCE, CLASS, RUNTIME
}
其中,
例如,@Retention(RetentionPolicy.RUNTIME)标注表示该注解在运行时有效。
@Documented
:
该注解用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被javadoc此类的工具文档化。Documented是一个标记注解,没有成员。关键源码如下:
public @interface Documented {
}
@Inherited
:
该注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的,描述注解是否被子类继承。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。关键源码如下:
public @interface Inherited {
}
注解本质上就是一个接口,该接口默认继承Annotation接口
public interface MyAnno extends java.lang.annotation.Annotation {}
通过javac编译, javap反编译
注解的作用:
JDK中预定义的一些注解:
@Override
:检测被该注解标注的方法是否是继承自父类(接口)的@Deprecated
:该注解标注的内容,表示已过时@SuppressWarnings
:压制警告,一般传递参数all @SuppressWarnings(“all”)定义一个注解及其属性:
public @interface MyAnn {
String value();
int value1();
}
// 使用注解MyAnn,可以设置属性
@MyAnn(value1=100,value="hello")
public class MyClass {
}
定义了属性,在使用时需要给属性赋值:
当定义一个注解之后,还需要一个注解处理器来执行注解的内部逻辑。注解处理器定义了注解的处理逻辑,涉及到反射机制和线程机制等。
反射是框架设计的灵魂。
反射机制是指在运行中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意一个方法和属性。即动态获取信息和动态调用对象方法的功能称为反射机制。
静态编译和动态编译:
反射机制的作用:
反射机制的好处:
缺点:
与反射相关的类:
获取Class类有三种基本方式:
(1)通过类名称.class来获取Class类对象:
多用于参数的传递
Class c = int.class;
Class c = int[ ].class;
Class c = String.class
(2)通过对象.getClass( )方法来获取Class类对象:
多用于对象的获取字节码的方式
Class c = obj.getClass( );
(3)通过类名称加载类Class.forName( ),只要有类名称就可以得到Class:
多用于配置文件,将类名定义在配置文件中。读取文件,加载类
Class c = Class.forName(“cn.ywq.Demo”);
注意:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
class 对象的功能:
* 获取功能:
1. 获取成员变量们
* Field[] getFields() :获取所有public修饰的成员变量
* Field getField(String name) 获取指定名称的 public修饰的成员变量
* Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
* Field getDeclaredField(String name)
2. 获取构造方法们
* Constructor<?>[] getConstructors()
* Constructor<T> getConstructor(类<?>... parameterTypes)
* Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
* Constructor<?>[] getDeclaredConstructors()
3. 获取成员方法们:
* Method[] getMethods()
* Method getMethod(String name, 类<?>... parameterTypes)
* Method[] getDeclaredMethods()
* Method getDeclaredMethod(String name, 类<?>... parameterTypes)
4. 获取全类名
* String getName()
* Field:成员变量
* 操作:
1. 设置值
* void set(Object obj, Object value)
2. 获取值
* get(Object obj)
3. 忽略访问权限修饰符的安全检查
* setAccessible(true):暴力反射
* Constructor:构造方法
* 创建对象:
* T newInstance(Object... initargs)
* 如果使用【空参数构造方法】创建对象,操作可以简化:Class对象的newInstance方法
* Method:方法对象
* 执行方法:
* Object invoke(Object obj, Object... args)
* 获取方法名称:
* String getName:获取方法名
以反射方式来创建对象的Demo:
public class Demo1 {
public static void main(String[] args) throws Exception {
String className = "com.ywq.User";
// 获取Class对象
Class clazz = Class.forName(className);
// 创建User对象
User user = (User)clazz.newInstance();
// 和普通对象一样,可以设置属性值
user.setUsername("yangwenqiang");
user.setPassword("19931020");
System.out.println(user);
}
}
class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}
OutOfMemoryError
Exception又分为了运行时异常和编译时异常:
NumberFormatException
;NullPointerException
(要访问的变量没有引用任何对象时,抛出该异常);ArithmeticException
(算术运算异常,一个整数除以 0 时,抛出该异常);ArrayIndexOutOfBoundsException
(下标越界异常)public string getMessage()
:返回异常发生时的简要描述public string toString()
:返回异常发生时的详细信息public string getLocalizedMessage()
:返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同public void printStackTrace()
:在控制台上打印 Throwable 对象封装的异常信息前边说到了异常Exception应该被捕获,我们可以使用try – catch – finally 来处理异常,并且使得程序恢复正常。
ClassNotFoundException
:使用例如Class.forName方法来动态的加载该类的时候,传入了一个类名,但是其并没有在类路径中被找到的时候,就会报ClassNotFoundException异常。出现这种情况,一般都是类名字传入有误导致的。NoClassDefFoundError
:如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。但是要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError。出现这种情况,一般是由于打包的时候漏掉了部分类或者Jar包被篡改已经损坏。java.lang.StackOverflowError
:如果一个线程在计算时,由于递归太深,所需要用到栈大小 > 配置允许最大的栈大小,那么Java虚拟机将抛出StackOverflowError,即栈溢出错误java.lang.OutOfMemoryError
:如果一个线程可以动态地扩展本机方法栈,并且尝试本地方法栈扩展(没有大于配置允许最大的栈大小),但是内存不足可以提供, 或者如果不能提供足够的内存来为新线程创建初始的堆(如new Object)。JVM不能分配给对象的创建空间,并且GC也不能够回收足够的空间。JVM空间溢出,创建对象速度高于GC回收速度。那么Java虚拟机将抛出OutOfMemoryError,即内存不足错误全名叫Just In Time Compile 也就是即时编译器,把经常运行的代码作为"热点代码"编译成与本地平台相关的机器码,并进行各种层次的优化。JIT编译除了具有缓存的功能外,还会对代码做各种优化,包括逃逸分析、锁消除、 锁膨胀、方法内联、空值检查消除、类型检测消除以及公共子表达式消除等。
逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。JIT编译器的优化包括同步省略和标量替换。
同步synchronized
去掉相关概念:
逃逸分析技术的缺点:
技术不是特别成熟,分析的过程也很耗时,如果没有一个对象是不逃逸的,那么就得不偿失了。
JVM -X参数
public class Test {
public static void main(String[] args) {
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
控制台输出:
x:小李
y:小张
s1:小张
s2:小李
方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝
Java 中方法参数的使用情况:
String 被声明为 final,因此它不可被继承。
在 Java 8 中,String 内部使用 char 数组
存储数据。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
在 Java 9 之后,String 类的实现改用 byte 数组
存储字符串,同时使用 coder 来标识使用了哪种编码。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value
数组的方法,因此可以保证 String 不可变
。
不可变的好处:
① 可变性
String 类中使用 final
关键字修饰字符数组
来保存字符串,private final char value[]
在 Java 9 之后,String 类的实现改用 byte 数组
存储字符串, private final byte[] value
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final
关键字修饰,所以这两种对象都是可变的。
② 线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf
等公共方法。
同步锁synchronized
,所以是线程安全的。StringBuffer 线程安全 效率低
记忆:fe safe
③ 性能
④ 总结
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如
此,还可以使用 String 的 intern() 方法
在运行过程中将字符串添加到 String Pool 中。
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法
进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回
这个新字符串的引用。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一
个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用
的是同一个字符串。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true
如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true
在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError
错误。
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。
使用泛型的好处:
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
拓展阅读:10 道 Java 泛型面试题
参考文章:
Java笔试面试-克隆和序列化
Java序列化与反序列化
内存中的数据对象只有转换成二进制流才能进行数据持久化
或者网络传输
,将对象转换成二进制流的过程叫做序列化(Serialization);相反,把二进制流恢复为数据对象的过程就称之为反序列化(Deserialization)。
为什么要用序列化与反序列化
对象序列化的两种用途:
Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient
只能修饰变量,不能修饰类和方法。
什么是序列化,如何实现序列化?
Java 中对象的序列化就是将对象转换成二进制序列
,反序列化则是将二进制序列转换成对象。
采用Java序列化与反序列化技术:
Java 实现序列化的多种方式
先把对象序列化到磁盘,再从磁盘中反序列化出对象,请参考以下代码:
class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 对象赋值
User user = new User();
user.setName("大冰");
user.setAge(20);
System.out.println(user);
// 创建输出流(序列化内容到磁盘)
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.out"));
// 序列化对象
oos.writeObject(user);
oos.flush();
oos.close();
// 创建输入流(从磁盘反序列化)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.out"));
// 反序列化
User user2 = (User) ois.readObject();
ois.close();
System.out.println(user2);
}
}
class User implements Serializable {
private static final long serialVersionUID = 3831264392873197003L;
private String name;
private int age;
@Override
public String toString() {
return "{name:" + name + ",age:" + age + "}";
}
// setter/getter...
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;
}
}
程序执行结果:
{name:大冰,age:20}
{name:大冰,age:20}
1.serialVersionUID 的作用是什么?
答:如果显示定义了 serialVersionUID 值之后,可以使序列化和反序列化向后兼容
。也就是说如果 serialVersionUID 的值相同,修改对象的字段(删除或增加),程序不会报错,之后给没有的字段赋值为 null,而如果没有指定 serialVersionUID 的值,如果修改对象的字段,程序就会报错。
2.可序列化接口(Serializalbe)的用途是什么?
答:可序列化 Serializalbe 接口存在于 java.io 包中,构成了 Java 序列化机制的核心,它没有任何方法,它的用途是标记某对象为可序列化对象
,指示编译器使用 Java 序列化机制序列化此对象。
3.常用的序列化方式都有哪些?
Java 原生序列化方式
JSON 格式,可使用 fastjson 或 GSON
Hessian 方式序列化
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
对任何不是 null 的对象 x 调用 x.equals(null)
结果都为 false
equals
是判断两个变量或者实例指向同一个内存空间的值是不是相同,
而==
是判断两个变量或者实例是不是指向同一个内存空间
==
:它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==
比较的是值,引用数据类型==比较的是内存地址)。
equals()
:它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true -----------------this is true------------------
System.out.println("true");
}
}
}
说明:
String
中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。Object.equals()
: 用来判断两个对象是否相同,在Object类中是通过判断对象间的内存地址来决定是否相同
Object.hashCode()
: 获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
先检查hashCode值,后判断equals()
面试官:你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法
?
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码(可以快速找到所需要的对象)
我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值
来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
通过我们可以看出:hashCode()
的作用就是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()在散列表中才有用,在其它情况下没用。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
重写 equals 时必须重写 hashCode 方法
。默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
clone() 是 Object 的 protected
方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类
实例的 clone() 方法。
public class CloneExample {
private int a;
private int b;
}
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
重写 clone() 得到以下实现:
public class CloneExample {
private int a;
private int b;
@Override
public CloneExample clone() throws CloneNotSupportedException {
return (CloneExample)super.clone();
}
}
CloneExample e1 = new CloneExample();
try {
CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
java.lang.CloneNotSupportedException: CloneExample
以上抛出了 CloneNotSupportedException
,这是因为 CloneExample 没有实现 Cloneable 接口。
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只
是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException
。
public class CloneExample implements Cloneable {
private int a;
private int b;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
拷贝对象和原始对象的引用类型引用同一个对象。
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
public class ShallowCloneExample implements Cloneable {
private int[] arr;
public ShallowCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected ShallowCloneExample clone() throws CloneNotSupportedException {
return (ShallowCloneExample) super.clone();
}
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//意思是e1和e2指向同一个对象
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
拷贝对象和原始对象的引用类型引用不同对象。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
public class DeepCloneExample implements Cloneable {
private int[] arr;
public DeepCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected DeepCloneExample clone() throws CloneNotSupportedException {
DeepCloneExample result = (DeepCloneExample) super.clone();
//创建一个新的对象,并复制其内容
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
result.arr[i] = arr[i];
}
return result;
}
}
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//意思是e1和e2指向不同的对象
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,
最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
public class CloneConstructorExample {
private int[] arr;
public CloneConstructorExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
//拷贝构造函数
public CloneConstructorExample(CloneConstructorExample original) {
arr = new int[original.arr.length];
for (int i = 0; i < original.arr.length; i++) {
arr[i] = original.arr[i];
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2,深拷贝
‘\0’
来表示结束。但是,Java 语言中没有结束符这一概念。java.lang.Object
,C++ 为了兼容 C 即支持面向对象也支持final 关键字主要用在三个地方:变量、方法、类。
数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是被引用的对象本身是可以修改的。
类
当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
方法
使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义,声明方法不能被子类重写;第二个原因是效率。
在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。
类中所有的 private 方法都隐式地指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
静态是指被static修饰符修饰的,包括类、方法、变量、块等。
1 静态变量
2 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
public abstract class A {
public static void func1(){
}
// public abstract static void func2();
// Illegal combination of modifiers: 'abstract' and 'static'
}
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。
public class A {
private static int x;
private int y;
public static void func1(){
int a = x;
// int b = y; // Non-static field 'y' cannot be referenced from a static context
// int b = this.y; // 'A.this' cannot be referenced from a static context
}
}
3 静态语句块
静态语句块在类初始化时运行一次。
public class A {
static {
System.out.println("123");
}
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}
123
4 静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要。
public class OuterClass {
//非静态内部类依赖于外部类的实例
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// InnerClass innerClass = new InnerClass();
// 'OuterClass.this' cannot be referenced from a static context
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
静态内部类不能访问外部类的非静态的变量和方法。
5 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
import static com.xxx.ClassName.*
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺
序。
public static String staticField = "静态变量";
static {
System.out.println("静态语句块");
}
public String field = "实例变量";
{
System.out.println("普通语句块");
}
最后才是构造函数的初始化。
public InitialOrderTest() {
System.out.println("构造函数");
}
存在继承的情况下,初始化顺序为:
静态特点:
1.一声明就被储存在栈中,直接占据内存,可以快速稳定的调用;
2.生命周期长,从JVM加载开始到JVM卸载结束;
3.全局唯一:在一个运行环境中,静态变量只有一个值,任何一次修改都是全局性影响;
4.占据内存,程序中应包含尽量少的static;
非静态是指没被static修饰的,特点:
1.new的时候占据内存,实例化后调用;
2.非静态变量赋值不发生冲突;
由于(静态方法)可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
类名.方法名
“的方式,也可以使用”对象名.方法名
"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型
就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
NIO 提供了与传统 BIO 模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
参考文章:
IO多路复用机制详解
同步和异步的概念描述的是用户线程与内核的交互方式:
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式: