Java 基本语法
1.1.1. 自动装箱、拆箱
什么是自动装箱、拆箱
简单一点说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。如下所示:
//自动装箱 Integer total = 99; //自动拆箱 int totalprim = total;
反编译 class 文件之后得到如下内容:
javap -c StringTest
Integer total = 99; // 执行上面那句代码的时候,系统为我们执行了: Integer total = Integer.valueOf(99); int totalprim = total; // 执行上面那句代码的时候,系统为我们执行了: int totalprim = total.intValue(); /** * 进而以 Integer 为例,分析源码 */ // Integer.valueOf 函数 public static Integer valueOf(int i) { return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128]; } // Integer 的构造函数: private final int value; // 定义了一个 value 变量,创建一个 Integer 对象,就会给这个变量初始化。 public Integer(int value) { this.value = value; } // 传入的是一个 String 变量,它会先把它转换成一个 int 值,然后进行初始化。 public Integer(String string) throws NumberFormatException { this(parseInt(string)); } // SMALL_VALUES[i + 128],它是一个静态的 Integer 数组对象,也就是说最终 valueOf 返回的都是一个 Integer 对象。 private static final Integer[] SMALL_VALUES = new Integer[256];
小结:装箱的过程会创建对应的对象,这个会消耗内存,所以装箱的过程会增加内存的消耗,影响性能。
进一步了解
在 Integer 的构造函数中,它分两种情况:
i >= 128 || i < -128 =====> new Integer(i) i < 128 && i >= -128 =====> SMALL_VALUES[i + 128]
private static final Integer[] SMALL_VALUES = new Integer[256];
SMALL_VALUES 本来已经被创建好,也就是说在 i >= 128 || i < -128 是会创建不同的对象,在 i < 128 && i >= -128 会根据 i 的值返回已经创建好的指定的对象。下面举例说明:
public class Main { public static void main(String[] args) { Integer i1 = 100; Integer i2 = 100; Integer i3 = 200; Integer i4 = 200; System.out.println(i1==i2); //true System.out.println(i3==i4); //false } }
- i1 和 i2 会进行自动装箱,执行了 valueOf 函数,它们的值在(-128,128]这个范围内,它们会拿到 SMALL_VALUES 数组里面的同一个对象 SMALL_VALUES[228],它们引用到了同一个 Integer 对象,所以它们肯定是相等的。
- i3 和 i4 也会进行自动装箱,执行了 valueOf 函数,它们的值大于 128,所以会执行 new Integer(200),也就是说它们会分别创建两个不同的对象,所以它们肯定不等。
举例 2:
public class Main { public static void main(String[] args) { Double i1 = 100.0; Double i2 = 100.0; Double i3 = 200.0; Double i4 = 200.0; System.out.println(i1==i2); //false System.out.println(i3==i4); //false } }
看看上面的执行结果,跟 Integer 不一样,对于 Integer,在(-128,128]之间只有固定的 256 个值,所以为了避免多次创建对象,我们事先就创建好一个大小为 256 的 Integer 数组 SMALL_VALUES,所以如果值在这个范围内,就可以直接返回我们事先创建好的对象就可以了。
但是对于 Double 类型来说,因为它在这个范围内个数是无限的。所以在 Double 里面的做法很直接,就是直接创建一个对象,所以每次创建的对象都不一样。
public static Double valueOf(double d) { return new Double(d); }
下面我们进行一个归类:
- Integer 派别:Integer、Short、Byte、Character、Long 这几个类的 valueOf 方法的实现是类似的。
- Double 派别:Double、Float 的 valueOf 方法的实现是类似的。每次都返回不同的对象。
其他情况
Boolean
public class Main { public static void main(String[] args) { Boolean i1 = false; Boolean i2 = false; Boolean i3 = true; Boolean i4 = true; System.out.println(i1==i2);//true System.out.println(i3==i4);//true } } // 返回的都是 true,也就是它们执行 valueOf 返回的都是相同的对象。 public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; } // 可以看到它并没有创建对象,因为在内部已经提前创建好两个对象,因为它只有两种情况,这样也是为了避免重复创建太多的对象。 public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false);
拆箱操作
Integer num1 = 400; int num2 = 400; System.out.println(num1 == num2); //true System.out.println(num1.equals(num2)); //true
equals 源码:
@Override public boolean equals(Object o) { return (o instanceof Integer) && (((Integer) o).value == value); }
说明:指定 equal 比较的是内容本身,并且我也可看到 equals 的参数是一个 Object 对象,我们传入的是一个 int 类型,所以首先会进行装箱,然后比较。之所以返回 true,是由于它比较的是对象里面的 value 值。
Integer num1 = 100; int num2 = 100; Long num3 = 200l; System.out.println(num1 + num2); //200 System.out.println(num3 == (num1 + num2)); //true System.out.println(num3.equals(num1 + num2)); //false
说明:当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,对基础数据类型进行运算。 对于 num3.equals(num1 + num2) 为 false 的原因,根据代码实现来说明。
@Override public boolean equals(Object o) { return (o instanceof Long) && (((Long) o).value == value); }
所以,对于 num3.equals(num1 + num2) 为 false 的原因就是类型不同。
陷阱
Integer integer100=null; int int100=integer100;
这两行代码是完全合法的,完全能够通过编译的,但是在运行时,就会抛出空指针异常。其中,integer100 为 Integer 类型的对象,它当然可以指向 null。但在第二行时,就会对 integer100 进行拆箱,也就是对一个 null 对象执行 intValue() 方法,当然会抛出空指针异常。所以,有拆箱操作时一定要特别注意封装类对象是否为 null。
1.1.2. 重写(override) 和重载(overload)
- 概念
- Override:子类中和父类中的方法声明一模一样。
- Overload:方法一样,参数或参数类型不一样。
- 使用注意事项
- 父类中私有方法不能被重写
- 子类重写父类方法时,访问权限不能更低
- 父类静态方法,子类也必须通过静态方法进行重写。(算不上重写)
- 推荐:重写时,最好声明一模一样。
1.1.3. 范型
- 概念
- 范型:在日常生活中,橡皮泥通过外力可以改变其形状,其形状是不固 定的。在 Java 中,通过泛型可以给开发带来方便,通过参数的指 定,可以改变其类型。
- 使用范型的优缺点
- 使代码看起来灵活,代码量减少,容易管理 ,不容易产生错误;
- 使用泛型在代码编译的时候能进行类型的检查并自动转换,使代码的运行效率得到提高;
- 使用泛型在编译进行自动转换的时候出现了错误,会进行错误提示;
-(缺点)在性能上不如数组快。
- 常用通配符
- 使用大写字母 A,B,C,D……X,Y,Z 定义的,就都是泛型,把 T 换成 A 也一样,这里 T 只是名字上的意义而已。
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
- ?和 T 区别是:?是一个不确定类,?和 T 都表示不确定的类型 ,但如果是 T 的话,函数里面可以对 T 进行操作,比方 T car = getCar(),而不能用? car = getCar()。
- Object 和 T 不同点在于,Object 是一个实打实的类,并没有泛指谁,可以直接给 List 中 add(new Object())
- Class<T> vs Class<?>
- Class 是什么呢,Class 也是一个类,但 Class 是存放上面 String,List,Map……类信息的一个类,有点抽象。
- 如何获取到 Class 类呢,有三种方式:
List list = null; Class clazz = list.getClass(); Class clazz = Class.forName("com.lyang.demo.fanxing.People"); Class clazz = List.class;
- 使用 Class<T>和 Class<?>多发生在反射场景下,如果我们不使用泛型,反射创建一个类是什么样的:
People people = (People) Class.forName("com.lyang.demo.fanxing.People").newInstance(); // 需要强转,如果反射的类型不是 People 类,就会报 java.lang.ClassCastException 错误。 // 使用 Class<T>泛型后,不用强转了 public class Test { public static <T> T createInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException { return clazz.newInstance(); } public static void main(String[] args) throws IllegalAccessException, InstantiationException { Fruit fruit= createInstance(Fruit .class); People people= createInstance(People.class); } }
- 结论:
- Class<T>在实例化的时候,T 要替换成具体类
- Class<?>它是个通配泛型,?可以代表任何类型,主要用于声明时的限制情况
// 可以这样声明 public Class<?> clazz; // 但不可以这样 public Class<T> clazz;
1.1.4. Java 参数传递问题
参考链接: https://blog.csdn.net/bjweimengshu/article/details/79799485
- 误解
- (1)值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。
- (2)Java 是引用传递。
- (3)传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。
- 实参和形参
- 形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。
- 实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。
- 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。值传递和引用传递
Table 1: 值传递和引用传递 值传递 引用传递 根本区别 会创建副本 不创建副本 结论 函数中无法改变原始对象 函数中可以改变原始对象 举例:
- Java 为什么只有值传递?
- 总结
- Java 中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用。可以说,Java 中的传递只有值传递。
- 无论是值传递还是引用传递,其实都是一种求值策略(Evaluation strategy)。在求值策略中,还有一种叫做按共享传递(call by sharing)。其实 Java 中的参数传递严格意义上说应该是按共享传递。
- 共享传递:指在调用函数时,传递给函数的是实参的地址的拷贝(如果实参在栈中,则直接拷贝该值)。在函数内部对参数进行操作时,需要先拷贝的地址寻找到具体的值,再进行操作。如果该值在栈中,那么因为是直接拷贝的值,所以函数内部对参数进行操作不会对外部变量产生影响。如果原来拷贝的是原值在堆中的地址,那么需要先根据该地址找到堆中对应的位置,再进行操作。因为传递的是地址的拷贝所以函数内对值的操作对外部变量是可见的。
1.1.5. 深拷贝和浅拷贝
1.1.5.1. 概念
- 浅拷贝(浅复制/浅克隆)
- 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所拥有的对象,而不复制它所引用的对象。
- 深拷贝(深复制/深复制)
- 被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
1.1.5.2. java Cloneable 接口(浅拷贝)
clone()
clone 方法将对象复制了一份并返回给调用者。一般而言,clone() 方法满足:
- 对任何的对象 x,都有 x.clone() !=x//克隆对象与原对象不是同一个对象
- 对任何的对象 x,都有 x.clone().getClass()==x.getClass()//克隆对象与原对象的类型一样
- 如果对象 x 的 equals() 方法定义恰当,那么 x.clone().equals(x) 应该成立。
Java 中对象的克隆
- 为了获取对象的一份拷贝,我们可以利用 Object 类的 clone() 方法。
- 在派生类中覆盖基类的 clone() 方法,并声明为 public。
- 在派生类的 clone() 方法中,调用 super.clone()。
- 在派生类中实现 Cloneable 接口。
代码实现
基于 Object#clone 方法实现对象复制,但是 Object#clone 是 protected 方法,在覆写时,需要改成 public 修饰。
class Address implements Cloneable { private String street; private String city; private String country; // standard constructors, getters and setters // ... ... @Override public Object clone(){ try { return super.clone(); } catch (CloneNotSupportedException e) { return new Address(this.street, this.getCity(), this.getCountry()); } } } class User implements Cloneable { private String name; private Address address; //standard constructors, getters and setters // ... ... @Override public Object clone() { User user; try { // super.clone 方法实现的是浅拷贝,因此返回的为浅拷贝对象。我们需要对于可变对象的属性,重新设置新的值。 user = (User) super.clone(); } catch (CloneNotSupportedException e) { user = new User(this.getName(), this.getAddress()); } user.address = (Address) this.address.clone(); return user; } }
import static org.junit.Assert.assertThat; Address address = new Address("西湖区丰潭路 380 号", "杭州", "中国"); User origin = new User("嗨皮的孩子", address); User shallowCopy = (User) origin.clone(); address.setStreet("西湖区丰潭路 381 号"); assertThat(shallowCopy.getAddress().getStreet(), not(equalTo(origin.getAddress().getStreet())));
1.1.5.3. 序列化方式(深拷贝)
把对象写到流里的过程是串行化(Serilization)过程;而把对象从流中读出来是并行化(Deserialization)过程。
public Object deepClone() { // 将对象写到流里 ByteArrayOutoutStream bo=new ByteArrayOutputStream(); ObjectOutputStream oo=new ObjectOutputStream(bo); oo.writeObject(this); // 从流里读出来 ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray()); ObjectInputStream oi=new ObjectInputStream(bi); return(oi.readObject()); }
1.1.5.4. 第三方库:使用 Apache Commons Lang(深拷贝,推荐)
Apache Commons Lang 有 SerializationUtils#clone 方法,使用该方法可以深度复制出对象图中所有实现 Serializable 接口对象。此方法要求对象中的类都实现 Serializable 接口,否则会抛出 SerializationException。
class Address implements Serializable, Cloneable { private String street; private String city; private String country; // standard constructors, getters and setters // ... ... @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { return new Address(this.street, this.getCity(), this.getCountry()); } } } class User implements Serializable, Cloneable { private String name; private Address address; //standard constructors, getters and setters // ... ... @Override public Object clone() { User user; try { user = (User) super.clone(); } catch (CloneNotSupportedException e) { user = new User(this.getName(), this.getAddress()); } user.address = (Address) this.address.clone(); return user; } }
import org.apache.commons.lang.SerializationUtils; Address address = new Address("西湖区丰潭路 380 号", "杭州", "中国"); User origin = new User("嗨皮的孩子", address); User shallowCopy = (User) SerializationUtils.clone(origin); address.setStreet("西湖区丰潭路 381 号");
1.1.5.5. 第三方库:使用 Gson 的 JSON 系列化(深拷贝,推荐)
使用 Json 系列化方式,也可以的达到深度复制,Gson 库可以用来将对象转为 JSON,或从 JSON 转为对象。Gson 不要求类实现 Serializable 接口。
import com.google.gson.Gson; Address address = new Address("西湖区丰潭路 380 号", "杭州", "中国"); User origin = new User("嗨皮的孩子", address); Gson gson = new Gson(); User shallowCopy = gson.fromJson(gson.toJson(origin), User.class); address.setStreet("西湖区丰潭路 381 号");
1.1.5.6. 第三方库:使用 Jackson 的 JSON 系列化(深拷贝,推荐)
Jackson 实现上类似于 Gson,但是我们需要给类提供默认构造函数。
import com.fasterxml.jackson.databind.ObjectMapper; Invoice invoice = new Invoice("id123","blue"); ObjectMapper objectMapper = new ObjectMapper(); // 加入此行代码,解决 "Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): // cannot deserialize from Object value (no delegate- or property-based Creator)" // Stack Overflow: https://stackoverflow.com/questions/45863678/json-parse-error-can-not-construct-instance-of-java-time-localdate-no-string-a objectMapper.registerModule(new JavaTimeModule()); String writeValueAsString = objectMapper.writeValueAsString(invoice); Invoice invoiceCopy = objectMapper.readValue(writeValueAsString, Invoice.class);
1.1.6. 反射
反射:在日常生活中,通过放大镜可以看清楚物体的内部结构。在 Java 中,反射机制就是起到放大镜的效果,可以通过类名,加载这个 类,显示出这个类的方法等信息。
/** * Java 反射 */ // 获取 class(方式 1) String path = "com.zrg.myReflection.bean.User"; Class clazz = Class.forName(path); // 获取 class(方式 2) Class clazz = User.class; // 获取 class(方式 3) User user = new User(); Class clazz = user.getClass(); // 获取类名 String strName01 = clazz.getName();// 获取完整类名 com.zrg.myReflection.bean.User String strName02 = clazz.getSimpleName();// 直接获取类名 User // 获取普通方法 Method[] Method01 = clazz.getDeclaredMethods(); // 返回 public 方法 Method method = clazz.getDeclaredMethod("getId", null); // 返回 getId 这个方法,如果没有参数,就默认为 null // 获取构造方法 User u1 = (User) clazz.newInstance(); // 获取无参的构造函数这里的前提的保证类中应该有无参的构造函数 // 获取参数为(int,String,int) 的构造函数 Constructor c2 = clazz.getDeclaredConstructor(int.class, String.class, int.class); // 通过有参构造函数创建对象 User u2 = (User) c2.newInstance(1001, "小小", 18); // 通过反射调用普通方法 User u3 = (User) clazz.newInstance(); Method method03 = clazz.getDeclaredMethod("setId", int.class); method.invoke(u3, 1002); // 把对象 u3 的 id 设置为 1002 // 通过反射操作普通的属性 User u4 = (User) clazz.newInstance(); Field f = clazz.getDeclaredField("name"); f.setAccessible(true); // 设置属性可以直接的进行访问 f.set(u4, "石头"); // 获取属性、属性值 Field[] fields = clazz.getDeclaredFields(); // 返回所有的属性 Field[] fields = clazz.getFields(); // 返回属性为 public 的字段 Field field = clazz.getDeclaredField("id"); // 获取属性为 id 的字段 for (String field : fields) { try { field.setAccessible(true); // 设置私有属性允许访问 Object value = field.get(clazz); // 获取属性值 }catch(Exception e){ e.printStackTrace(); } }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: MySQL 正则模式
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论