深入学习java源码之String.concat()与String.substring()
final变量:
对于基本类型使用final:它就是一个常量,数值恒定不变
对于对象引用使用final:使得引用恒定不变,一旦引用被初始化指向一个对象,就无法再把 它改为指向另一个对象。然而,对象自身却是可以被修改的,java并没有提供使任何对象恒定不变的途径。这一限制同样也使用数组,它也是对象。
class Value{
int i;
public Value(int i){
this.i = i;
}
}
public class FinalData {
private static Random random = new Random(47);
private String id;
public FinalData(String id){
this.id = id;
}
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
public static final int VALUE_THREE = 39;
private final int i4 = random.nextInt(20);
static final int INT_5 = random.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
private final int[] a = {1, 2, 3, 4, 5, 6};
public String toString(){
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; // 因为valueOne是基本类型常量,其数值恒定不变
fd1.v2.i++; //final修饰的对象的内容可以改变
fd1.v1 = new Value(9);
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++;
//! fd1.v2 = new Value(0); // 因为v2是final修饰的引用类型,其引用不能被修改指向另一个对象
//! fd1.VAL_3 = new Value(1); // 表示占据一段不能改变的内存空间
//! fd1.a = new int[3]; // final修饰的数组
System.out.println(fd1);
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
/*output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*/
分析:
对于fd1,fd2两个对象,其中i4是唯一的,即每个对象都有一个i4,但INT_5被声明为static,即是类共享的,fd1和fd2共享INT_5,在装载时已经被初始化,而不是每次创建新对象时初始化(例如i4);但它同时被设置成final,所以它的引用是不可改变的,即不能被修改指向另一个对象。
空白final:
被声明为final但又没有给定初值。必须在域的定义或者每个构造器中使用表达式对final进行赋值,这正是final域在使用前总是初始化的原因。
final参数:
这意味着你无法在方法中更改参数引用,使其指向另一个参数,但可以修改final对象所指向的内容
class Gizmo{
int i = 0;
public void spin(){}
}
public class FinalArguments {
void with(final Gizmo g){
//! g = new Gizmo(); // 无法修改final修饰的引用,使它指向另一个对象
g.i++; // 但可以修改final对象所指向的内容
}
void without(Gizmo g){
g = new Gizmo();
g.spin();
}
// int g(final int i){
// //! i++; //因为参数i是常量值
// }
int g(final int i){
return i + 1;
}
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
}
分析:
参数被声明为final,若是基本参数,那它就是一个常量,不能被修改;若是一个引用变量,那么它就不能被修改指向另一个对象,但可以修改该引用所指对象的内容。
fianl方法:
使用原因:
- 把方法锁定,以防任何继承类修改它的含义,即该方法不会被继承的类覆盖
- 效率,若一个方法指明为final,那么就同意编译器将针对该方法的所有调用转为内嵌调用。
类中所有的private方法都隐式地指定为final,由于无法取用private方法,所以也就无法覆盖它。可以对private方法添加final修饰词,但这并不会给该方法带来任何额外的意义。
class WithFinals{
private final void f(){
System.out.println("WithFinals.f()");
}
private void g(){
System.out.println("OverridingPrivate.f()");
}
}
class OverridingPrivate extends WithFinals{
private final void f(){
System.out.println("OverridingPrivate.f()");
}
private void g(){
System.out.println("OverridingPrivate.g()");
}
}
class OverridingPrivate2 extends OverridingPrivate{
/*
* 当使用Override注解强制使f()方法覆盖父类的f()方法时,会报错
* 因为它不知道父类是否有该方法,对于g()方法来说,它只是生成了一个新的方法,
* 并没有覆盖掉父类中的g()方法。
*/
//@Override
public final void f(){
System.out.println("OverridingPrivate2.f()");
}
public void g(){
System.out.println("OverridingPrivate2.g()");
}
}
public class FinalOverridingIllusion{
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// 可以向上转型
OverridingPrivate op = op2;
//! op.f(); // 父类中final方法对子类来说是不可见的
//! op.g();
WithFinals wf = op2;
// wf.f();
// wf.g();
}
}
/*output:
OverridingPrivate2.f()
OverridingPrivate2.g()
*/
分析:
覆盖何时发生:
1,子类中出现与父类完全一致的方法
2. 子类可以通过向上转型为父类,并调用父类中的那个方法
若父类中某个方法被声明为final或者private,那么这个方法对子类来说是不可见的,就算在子类中创建了与父类一模一样的方法,这也是一个新的方法,而不是从父类中覆盖的方法。
final类:
即该类不能被继承,不管是你还是别人,也就是这个类不需要做任何变动,也不需要任何子类,例如String类。
class SmallBrain{}
final class Dinosaur{
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f(){}
}
// error: The type Further cannot subclass the final class Dinosaur
// Dinosaur类不能有子类
// class Further extends Dinosaur{}
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
}
String类的解析
构造一个字符串对象的方式有几种。先看第一种构造器:
private final char value[];
public String() {
this.value = "".value;
}
String源码中第一个私有域就是value这个字符数组,该数组被声明为final表示一旦初始化就不能被改变。也就是说一个字符串对象实际上是由一个字符数组组成的,并且该数组一旦被初始化则不能更改。这也很好的解释了String对象的一个特性:不可变性。一经赋值则不能改变。而我们第一种构造器就很简单,该构造器会将当前的string对象赋值为空(非null)。
接下来的几种构造器都很简单,实际上都是操作了value这个数组,但都不是直接操作,因为它不可更改,所以一般都是复制到局部来实现的各种操作。
第一种的传入一个String类型,还是第二种的直接传入char数组的方式,都是转换为为当前将要创建的对象中value数组属性赋值。至于第三种方法,对传入的char数组有要求,它要求从该数组索引位置为offset开始的后count个字符组成新的数组作为参数传入。该方法首先做了几个极端的判断并增设了对应的异常抛出,核心方法是Arrays.copyOfRange这个方法,它才是真正实现字符数组拷贝的方法。
该方法传入三个参数,形参value,起始位置索引,终止位置索引。在该方法中主要做了两件事情,第一,通过起始位置和终止位置得到新数组的长度,第二,调用本地函数完成数组拷贝。
System.arraycopy(original, from, copy, 0,Math.min(original.length - from, newLength));
虽然该方法是本地方法,但是我们大致可以猜出他是如何实现的,无非是通过while或者for循环遍历前者赋值后者。我们看个例子:
public static void main(String[] args){
char[] chs = new char[]{'w','a','l','k','e','r'};
String s = new String(chs,0,3);
System.out.println(s);
}
输出结果:wal
构建String对象的方式中,基本都是属于操作它内部的字符数组来实现的,下面的几种构造器则是通过操作字节数组来实现对字符串对象的构建,当然这些操作会涉及到编码的问题。下面我们看第一个有关字节数组的构造器:
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
该方法首先保证charsetName不为null,然后调用checkBounds方法判断offset、length是否小于0,以及offset+length是否大于bytes.length。然后调用一个核心的方法用于将字节数组按照指定的编码方式解析成char数组
StringCoding类的static char[] decode(String charsetName, byte[] ba, int off, int len)
首先通过deref方法获取对本地解码器类的一个引用,接着使用三目表达式获取指定的编码标准,如果未指定编码标准则默认为 ISO-8859-1,然后紧接着的判断主要是:如果未能从本地线程相关类中获取到StringDecoder,或者与指定的编码标准不符,则手动创建一个StringDecoder实例对象。最后调用一个decode方法完成译码的工作。相比于该方法,我们更常用以下这个方法来将一个字节数组转换成char数组。
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
只指定一个字节数组和一个编码标准即可,当然内部调用的还是我们上述的那个构造器。当然也可以不指定任何编码标准,那么则会使用默认的编码标准:UTF-8
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
当然还可以更简洁:
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
但是一般用于转换字节数组成字符串的构造器还是使用由字节数组和编码标准组成的两个参数的构造器。
以上为String类中大部分构造器的源代码,有些源码和底层操作系统等方面知识相关联,理解不深,见谅。下面我们看看有关String类的其他一些有关操作。
二、属性状态的常用函数 该分类的几个函数还是相对而言较为简单的,主要有以下几个函数:
//返回字符串的长度
public int length() {
return value.length;
}
//判断字符串是否为空
public boolean isEmpty() {
return value.length == 0;
}
//获取字符串中指定位置的单个字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
三、获取内部数值的常用函数 此分类下的函数主要有两大类,一个是返回的字符数组,一个是返回的字节数组。我们首先看返回字符数组的方法。
public static void main(String[] args){
String str = "hello-walker";
char[] chs = new char[6];
str.getChars(0,5,chs,1);
for(int a=0;a
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?