final关键字相关知识

目录

一、final关键字

final关键字是否能保证变量的可见性?

java中final、finally和 finalize 各有什么区别?

二、不可变类

关键特征

优缺点

举例:String

三、String类

String类是怎么保证不可变的?

String类为什么被设计为final类?

分析String类可变了对java容器有什么影响?

分析String类可变了对字符串常量池有什么影响

String s1 = new String("abc");这句话创建了几个字符串对象?

字符串拼接用“+” 还是 StringBuilder?

String 类型的变量和常量做“+”运算时发生了什么?

举例


一、final关键字

在Java中,final关键字用于表示一个不可变的常量或一个不可变的变量。

在Java中,final关键字可以修饰类、方法和变量,作用如下:

  1. final修饰类,表示该类不能被继承。final类中的方法默认都是final的,不能被子类重写。
  2. final修饰方法,表示该方法不能被子类重写。
  3. final修饰变量,表示该变量只能被赋值一次。final修饰的变量必须在声明时或构造函数中初始化,且不能再被修改。常用于定义常量。

另外,使用final修饰的变量在编译时就已经确定了其值,因此在运行时访问时比非final变量更快
使用final关键字可以带来一些好处,例如:

  • 安全性:将变量声明为final可以防止它被改变,从而提高安全性。
  • 可读性:将常量声明为final可以提高代码的可读性,因为常量的值不会被修改。
  • 优化:final变量在编译时被转换成常量,这可以提高程序的性能。

final关键字是否能保证变量的可见性?

不可以。

一般而言我们指的可见性是一个线程修改了共享变量,另一个线程可以立马得知更改,得到最新修改后的值。而final并不能保证这种情况的发生,volatile才可以。

而有些答案提到的final可以保证可见性,其实指的是final修饰的字段在构造方法初始化完成,并且期间没有把this传递出去,那么当构造器执行完毕之后,其他线程就能看见final字段的值。

如果不用final修饰的话,那么有可能在构造函数里面对字段的写操作被排序到外部,这样别的线程就拿不到写操作之后的值。

来看个代码,

public class YesFinalTest {
   final int a; 
   int b;
   static YesFinalTest testObj;

   public YesFinalTest () { //对字段赋值
       a = 1;
       b = 2;
   }

   public static void newTestObj () {  // 此时线程 A 调用这个方法
       testObj = new YesFinalTest ();
   }

   public static void getTestObj () {  // 此时线程 B 执行这个方法
       YesFinalTest object = testObj; 
       int a = object.a; //这里读到的肯定是 1
       int b = object.b; //这里读到的可能是 2
   }
}

下面是对这段 Java 代码的详细分析,重点关注final关键字的作用、对象的初始化、线程安全等方面。

(1)a 是一个final变量,表示该变量在初始化后不能再改变。在构造函数中对其赋值。b 是一个普通的实例变量,可以在对象的生命周期内被修改。testObj 是一个静态变量,属于类而不是任何实例。它用于存储YesFinalTest类的一个实例。

(2)线程 A 调用这个newTestObj()方法,它会在内存中创建一个新的YesFinalTest对象,并将其引用存储在静态变量 testObj 中。

当创建YesFinalTest的实例时,会执行构造函数。在构造函数中,a 被赋值为 1,而 b 被赋值为 2。由于 a 是 final,所以在构造器中初始化后,不可以再修改。

(3)当线程 B 调用getTestObj()方法时,它会读取静态变量 testObj 的值。 由于 a 是 final 变量,并且在构造器中已被赋值为 1,因此无论何时访问 a,都将得到 1

变量 b 的值可能是 2,但由于 b 不是 final,因此在不同线程的情况下,线程 B 可能在读取 b 时,线程 A 还没有完全初始化 testObj,导致可能会读到未初始化的 b,或者在一个线程中修改 b 的值,造成竞态条件。

这是因为,在多线程环境中,newTestObj 和 getTestObj 方法可以同时被不同的线程调用。若没有适当的同步机制,线程 B 可能会在 testObj 被线程 A 初始化之前读取 testObj,这可能导致 b 的值在某些情况下是未定义的。

而且testObj 是一个静态变量,且没有使用任何同步机制,线程 B 读取 testObj 时可能会看到一个未完全构造的对象,尤其是变量 b

在Java多线程环境中,对象发布指的是将一个对象的引用首次暴露给其他线程的过程。如果一个对象还没有完全构造好就被提前暴露出去,其他线程可能会看到一个未完成的对象状态,这就是所谓的”引用逃逸“。

  1. 构造函数内的写操作与对象引用赋值之间不能重排序
    1. 当你在构造函数中初始化一个final字段时,这个写操作不能被重排序到构造函数外部。即构造函数完成之前,对final字段的写操作必须完成。
    2. 例如,如果你在一个构造函数中给final字段赋值,然后发布这个对象的引用,那么这个赋值操作必须在发布对象引用之前完成。
  2. 对象引用的读取与final宇段的初次读取之间不能重排序
    1. 如果一个线程获取了一个对象的引用,然后去读取这个对象的某个final字段,这两个操作之间也不能重排序。
    2. 例如,如果线程B获取了线程A发布的对象引用,并且随后读取了该对象的某个final字段,那么获取引用的操作必须在读取final字段之前完成。

(4)总结

  • final 变量 a 在构造函数中初始化后不能更改,而普通变量 b 可能在不同线程中看到不同的值。
  • testObj 是一个静态变量,涉及多线程访问时应考虑同步问题,以确保线程安全和可见性。
  • 代码存在潜在的线程安全问题,建议在多线程环境下使用适当的同步机制(如 synchronized 关键字或其他并发控制手段)来确保 testObj 被完全初始化后再供其他线程使用。如果没有这样的机制,可能导致读取未完全初始化的对象状态。

这才是final的可见性,这种可见性和我们在并发中常说的可见性不是一个概念!

所以final无法保证可见性!

java中final、finally和 finalize 各有什么区别?

final、finally和finalize虽然长得像孪生兄弟一样,但是它们的含义和用法却是大相径庭。final是Java中的一个关键字,修饰符;finally是Java的一种异常处理机制;finalize是Java中的一个方法名。接下来,我们具体说一下他们三者之间的区别。

(1)final

final用于修饰类、方法、和变量,主要用来设计不可变类、确保类的安全性、优化性能(编译器优化)。

  • 类:被final修饰的类不能被继承。
  • 方法:被final修饰的方法不能被重写。
  • 变量:被final修饰的变量不可重新赋值,常用于定义常量。

(2)finally

finally与try-catch 语句块结合使用,用于确保无论是否发生异常,finally 代码块都会执行。
主要用于释放资源(如关闭文件、数据库连接等),以保证即使发生异常,资源也会被正确释放。

finally不执行的情况:

  • finally之前虚拟机终止运行
  • 程序所在的线程终止
  • CPU关闭

注意事项:不推荐在finally中使用return,这样会覆盖try 块中的返回值,容易引发难以发现的错误。 

(3)finalize()

finalize()是object类中的方法,允许对象在被垃圾回收前进行清理操作。

较少使用,通常用于回收非内存资源(如关闭文件或释放外部资源),但不建议依赖于它,因为VM不保证finalize()会被及时执行。

这是因为,当VM检测到对象不可达时,会标记对象,标记后将调用finalize()方法进行清理(如果重写了该方法),之后才会真正回收对象。但JVM并不承诺一定会等待finalize()运行结束,因此可能会造成内存泄漏或性能问题,所以在实际开发中,尽量避免使用finalize()进行清理操作。

在JDK9之后::finalize()方法已被标记为废弃,因为Java 提供了更好的替代方案(如Autocloseable 接口和try-with-resources语句)。

Java7引入的try-with-resources,它比依赖finalize()更加安全有效,能够自动关闭实现Autocloseable接口的资源。因此推荐使用try-with-resources。或者可以依赖对象生命周期管理机制(如Spring的DisposableBean)来实现更精细的资源回收。

二、不可变类

在Java中,不可变类是指在创建后其状态(对象的字段)无法被修改的类。一旦对象被创建,它的所有属性都不能被更改。

不可变类的实例在创建后不能被修改。任何对该对象的修改都会返回一个新的对象,而不会改变原有对象的属性值。

这种类的实例在整个生命周期内保持不变。

关键特征

  1. 声明类为final,防止子类继承。
  2. 类的所有字段都是private和final,确保它们在初始化后不能被更改。
  3. 通过构造函数初始化所有字段。
  4. 不提供任何修改对象状态的方法(如setter方法)。
  5. 如果类包含可变对象的引用,确保这些引|用在对象外部无法被修改。例如getter方法中返回对象的副本(new一个新的对象)来保护可变对象。

优缺点

优点:

  • 线程安全:由于不可变对象的状态不能被修改,它们天生是线程安全的,在并发环境中无需同步。
  • 缓存友好:不可变对象可以安全地被缓存和共享,如String的字符串常量池。
  • 防止状态不一致:不可变类可以有效避免因意外修改对象状态而导致的不一致问题。

缺点:性能问题:不可变对象需要在每次状态变化时创建新的对象,这可能会导致性能开销,尤其是对于大规模对象或频繁修改的场景(例如String频繁拼接)。

举例:String

以下是一些Java中常见的不可变类:

  • String:Java中的String类是不可变的,一旦创建了String对象,其值就不能被修改。
  • Integer,Long,Double:Java中的包装类lnteger、Long和Double也是不可变的,对象一旦创建后,其值也不能被修改。
  • BigDecimal,Biglnteger:BigDecimal和Biglnteger类用于高精度计算,它们也是不可变的。
  • LocalDate,LocalTime,LocalDateTime:Java 8中的日期时间类LocalDate、LocalTime和LocalDateTime也是不可变的。
  • Enum:枚举类在Java中也是不可变的,枚举常量在类加载的时候被创建,之后不能修改。
  • Collections.unmodifiableXXX:Java提供了Collections类中的一系列静态方法用于创建不可变集合,如Collections.unmodifiableListO,Collections.unmodifiableSetO,Collections.unmodifiableMapO等。
  • ZonedDateTime,OffsetDateTime:Java8中的日期时间类ZonedDateTime和OffsetDateTime也是不可变的。

Java中的经典不可变类有:String、Integer、BigDecimal、LocalDate等。

String就是典型的不可变类,当你创建一个String对象之后,这个对象就无法被修改。

因为无法被修改,所以像执行s+=“a";这样的方法,其实返回的是一个新建的String对象,老的s指向的对象不会发生变化,只是s的引用指向了新的对象而已。

三、String类

String类是怎么保证不可变的?

String类的不可变性是通过私有化内部数据、提供的API不修改自身、使用final关键字、以及字符串常量池等机制实现的。这些特性确保了String对象在创建后其内容不能被改变,从而提供了安全性和性能优势。

(1)String类被声明为final,这意味着无法继承或修改其实现。这种设计使得String的行为是固定的,进一步增强了其不可变性。

(2)String类的字符数组是私有的,外部无法直接访问和修改。这意味着即使用户持有一个String对象的引用,他们也无法改变这个对象的内容。

(3)String没有任何可以修改自身内容的方法。所有用于创建新字符串的方法(如concat()replace()toUpperCase()等)都返回一个新的String对象,而不会改变原有的String对象。

String类是如何保证成员变量 private final char[] value值不可改变?这里的final只是保证value变量不能指向别的内存地址,但却无法保证value[]内的元素不被更改。而真正使value[]值不可改变的,是String类中的所有方法都写得非常仔细,不存在任何一个方法对value[]内部元素进行更改。

(4)Java使用字符串常量池来存储字符串字面量。

每次创建字符串字面量时,Java首先检查常量池中是否已经存在相同的字符串。如果存在,则返回已有的对象,而不是创建新的对象。这种机制保证了对于相同内容的字符串,只有一个实例,从而节省内存并提高性能。

(5)由于String是不可变的,多个线程可以安全地共享相同的String实例,而无需额外的同步。这避免了由于状态变化导致的并发问题。

所以,真正使string类被设计成final的原因,是因为源代码中每个方法都没有改变过value[]值,这也是保证String不可变性的关键。而一旦不是final,string被继承,而继承的类重写或新增的方法一旦改变了value[] 的元素,整个string的特性将不存在。

String类为什么被设计为final类?

在Java中,String类被设计为final类,主要有以下几个原因:

(1)安全性

String类是不可变的,这意味着一旦创建,其值就不能被改变。

不可变的字符串可以被安全地共享和传递,尤其在多线程环境中。String作为常用的数据类型,被广泛使用在如参数传递、键值存储等场合。由于String对象是不可变的,多个线程可以同时读取相同的字符串实例,而不必担心数据被改变。这减少了并发编程中的复杂性和潜在的错误。

(2)性能

String类是不可变的,这意味着它的实例可以被多个线程安全地共享,而不需要使用额外的同步机制。这对于性能来说是非常重要的,因为在多线程环境中,频繁的同步操作可能会导致性能下降。

(3)避免继承导致的问题

String类声明为final意味着无法继承该类。这对于保持String的不可变性非常重要。如果String类可以被继承,子类可能会重写某些方法,从而改变其原有的行为,进而影响到不可变性。例如,子类可能允许修改字符串内容,导致数据的不一致性。

这将导致混淆,因为你可能不确定一个String对象的行为是由String类本身定义的,还是由它的子类定义的。通过将String类设计为final,可以避免这种混淆。

并且将String设计为final类,使得其设计和实现相对简单。开发者不需要考虑继承后的复杂性和可能的兼容性问题。开发者可以放心地使用String类的所有功能而不必担心不必要的复杂性。

(4)API一致性

Java的标准库中,很多使用字符串的API和类(如StringBuilderStringBuffer等)都是围绕着String的不可变性和final设计来构建的。这种一致性确保了API的预测性和可理解性,方便开发者使用。

总的来说,String类被设计为final是为了保证不可变性、安全性和性能优化,同时简化类的设计和使用。这样的设计使得String在Java编程中成为一种可靠和高效的基本数据类型。

分析String类可变了对java容器有什么影响?

(1)新建s,赋值hello。新建s2赋值gello。两者的hashcode方法得到的值不同。

(2)使用反射机制对s的成员变量value[]进行改变,将hello改成gello。

修改了s的成员变量value[],将第一位h换成了g,输出s,可以看到s变成了gello。

(3)对比更改后的s和s2的hashcode方法和equals方法

可以看到,更改后的s和s2的hashcode值不相同,而equals的值却相同。 

(4)由此可以看出,string类可变的话,将违背java设计规则。

通过前三个步骤可以看出,一旦string类可改变了。将违背java设计规则,即hashcode不同,equals一定也不同。hashcode相同,equals可能相同。

违背设计规则后,很多类的特性将失效。比如set集合,是不允许出现重复数据的。而强行改变s的值后,set不许出现重复数据的特性将失效。

hashset.add方法调用的就是hashmap的put方法,所以hashmap的键不能重复的特性一样会失效。

分析String类可变了对字符串常量池有什么影响

(1)String string="abc"和String string3=new String(“abc”)有什么区别

前者将对象abc存储于字符串常量池中,字符串常量池内重复元素只会创建一次,避免了对象反复创建销毁,下次再出现abc则不会新建对象,会将变量直接指向原有的abc。

而后者会将新建的对象存储于堆内存中,下次再出现abc任然会进行新建。如图所示,==符号对引用数据类型比较的是地址值。

可以看到,string2不会再创建新的对象。 

(2)String类可变了将破坏字符串常量池特性

可以看到,虽然s和s2的值是一样的,但是两者在常量池中的地址却是不一样的。这破坏了常量池的特性,即同一字符串在常量池中只会创建一次。

String s1 = new String("abc");这句话创建了几个字符串对象?

会创建 1 或 2 个字符串对象。

1)如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。

ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。

2)如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

字符串拼接用“+” 还是 StringBuilder?

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。

可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。

因为无法被修改,所以像执行s+=“a";这样的方法,其实返回的是一个新建的String对象,老的s指向的对象不会发生变化,只是s的引用指向了新的对象而已。所以不要在字符串拼接频繁的场景使用+来拼接,因为这样会频繁的创建对象。

如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。

String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++)  s += arr[i];
System.out.println(s);

// --->
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr)  s.append(value);
System.out.println(s);

String 类型的变量和常量做“+”运算时发生了什么?

比较 String 字符串的值是否相等,可以使用 equals() 方法。

String.equals() 方法是被重写过的。 Object.equals() 方法是比较的对象的内存地址,而 String.equals() 方法比较的是字符串的值是否相等。如果你使用 == 比较两个字符串是否相等的话,IDEA 还是提示你使用 equals() 方法替换。

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量
  • final 修饰的基本数据类型和字符串变量
  • 字符串通过 “+”拼接得到的字符串:实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
  • 基本数据类型之间算数运算(加减乘除)
  • 基本数据类型的位运算(<<、>>、>>> )
  • 引用的值在程序编译期是无法确定的,编译器无法对其进行优化。

举例

分析下面这段这段Java代码时,重点关注字符串的创建、拼接、以及==运算符的使用。以下是逐行分析:

String str1 = "str"; // 字符串常量池中创建字符串"str"
String str2 = "ing"; // 字符串常量池中创建字符串"ing"
String str3 = "str" + "ing"; // 在编译时,编译器优化,将"str"和"ing"直接拼接成"string",并在常量池中创建"string"
String str4 = str1 + str2; // 运行时拼接,创建新的字符串对象,内容为"string",但在堆内存中
String str5 = "string"; // 字符串常量池中已存在的"string"

代码的执行结果如下, 

对于 String str3 = "str" + "ing"; 编译器会给你优化成 String str3 = "string"; ,因此str3 是在编译时生成的常量 "string",存在于字符串常量池中。而str4 是在运行时通过拼接 str1 和 str2 创建的,位于堆内存中。由于 == 比较的是对象的引用,str3 和 str4 的引用不同,因此返回 false

str3 和 str5 都是指向字符串常量池中的 "string"。因为它们指向相同的对象,== 返回 true

str4 是通过拼接生成的新字符串对象,存储在堆内存中。str5 是指向字符串常量池中的 "string"。它们的引用不同,因此返回 false

总结一下,

  • 在Java中,使用 == 比较字符串时,它比较的是对象的引用而非内容。
  • 在这段代码中,str3 和 str5 指向相同的常量池对象,而 str4 是一个新的字符串对象,导致三者的比较结果不同。
  • 为了比较字符串的内容,应该使用 str1.equals(str2) 方法,而不是 == 运算符。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值