本小节我们将学习如何使用 Java 语言结合数据库进行编程。注意,学习本小节需要你有一定的 SQL 基础,了解 MySQL 数据库的 基础 CRUD 操作,如果你还不了解 SQL ,推荐先去学习一个非常不错的 wiki 教程,只需掌握前几节的 SQL 初级知识即可。本小节我们将选择开源免费的 MySQL 5.7 作为数据库,可以去官网下载并安装 MySQL,如果你不知如何下载安装,推荐按照这篇文章来做。通过本小节的学习,你将了解到什么是 JDBC,如何连接数据库,如何关闭数据库,JDBC 的新增、查询、更新和删除接口,如何执行批量等内容。
明确回答 Kotlin 中的实化类型参数函数不能在 Java 中的调用,我们可以简单的分析下,首先 Kotlin 的实化类型参数函数主要得益于 inline 函数的内联功能,但是 Java 可以调用普通的内联函数但是失去了内联功能,失去内联功能也就意味实化操作也就化为泡影。故重申一次 Kotlin 中的实化类型参数函数不能在 Java 中的调用。
每个事物都有其生命周期,也就是事物从出生开始到最终消亡这中间的整个过程。在其整个生命周期的历程中,会有不同阶段,每个阶段对应着一种状态,比如:人的一生会经历从婴幼儿、青少年、青壮年、中老年到最终死亡,离开这人世间,这是人一生的状态。同样的,线程作为一种事物,也有生命周期,在其生命周期中也存在着不同的状态,不同的状态之间还会有互相转换。Java 线程的声明周期会经历 6 中不同的状态变化,后续章节会有详细描述。从线程的创建到线程执行任务的完成,即 Java 线程的生命周期。
一直以来,我们都在向屏幕输出内容以验证我们编写的代码逻辑。向屏幕输出内容非常简单,可以由以下两种方式来完成:// 打印 Hello World,不换行System.out.print("Hello World");// 打印 Hello Java,并换行System.out.println("Hello Java");
每个 Java 版本都有该版本特有的特性,而如果要在 Eclipse 中使用这些特有的特性,我们需要设置好编译器的特性级别。这项功能在 Eclipse 中的设置位于首选项中的 Java > Compiler 中。通常,我们都建议设置该级别和我们当前设置的 JRE 版本相同,如下图所示:
定义: 在 Java 线程的生命周期中,它要经过新建(New),运行(Running),阻塞(Blocked),等待(Waiting),超时等待(Timed_Waiting)和终止状态(Terminal)6 种状态。从线程的新建(New)到终止状态(Terminal),就是线程的整个生命周期。Tips :与操作系统相比, Java 线程是否少了 “就绪” 状态 ?其实 Java 线程依然有就绪状态,只不过 Java 线程将 “就绪(Runnable)" 和 “运行(Running)” 两种状态统一归结为 “运行(Running)” 状态。我们来看下 Java 线程的 6 种状态的概念。新建 (New):实现 Runnable 接口或者继承 Thead 类可以得到一个线程类,new 一个实例出来,线程就进入了初始状态。运行 (Running):线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一方式。阻塞 (Blocked):阻塞状态是线程在进入 synchronized 关键字修饰的方法或者代码块时,由于其他线程正在执行,不能够进入方法或者代码块而被阻塞的一种状态。等待 (Waiting):执行 wait () 方法后线程进入等待状态,如果没有显示的 notify () 方法或者 notifyAll () 方法唤醒,该线程会一直处于等待状态。超时等待 (Timed_Waiting):执行 sleep(Long time)方法后,线程进入超时等待状态,时间一到,自动唤醒线程。终止状态 (Terminal):当线程的 run () 方法完成时,或者主线程的 main () 方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
首先看下图,图中展示了Java 的内存模型。工作内存(私有):由于JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(栈空间),用于存储线程私有的数据。线程私有的数据只能供自己使用,其他线程不能够访问到当前线程私有的内存空间,保证了不同的线程在处理自己的数据时,不受其他线程的影响。主内存(共享):Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。从上图中可以看到,Java 的并发内存模型与操作系统的 CPU 运行方式极其相似,这就是 Java 的并发编程模型。通过创建多条线程,并发的进行操作,充分利用系统资源,达到高效的并发运算。
先回顾一下纯手工 Jdbc 操作流程:加载由不同厂商遵循 Jdbc 规范开发的驱动类。关于 Jdbc 驱动类开发不是这里要讲的,可查阅相关资料;Class.forName("对应数据库的驱动类名");建立 Java 程序和数据库系统的连接。本质是进程和进程的网络连接;Connection conn = DriverManager.getConnection("url","用户名","密码");确定数据清单。数据库只认 SQL 语句,你需要数据库帮你做什么样的数据操作,需要传送 SQL 指令给数据库;String sql="使用sql语法描述数据需求";需要一个信使。创建一个语句处理对象充当信使,任务就是上传下达;PreparedStatement preparedStatement=conn.prepareStatement(sql); 信使工作,把程序中的数据搬运到数据库,或把数据库数据搬运到程序;preparedStatement.各种方法();数据搬运过来后,Java 代码要用呀!Java 语言有什么特点?面向对象吗?搬运过来的数据是符合关系数据库特点的数据,于是开始手工数据格式转换、封装;省略若干代码,心里希望表结构中字段不要太多!此时的苦只有自己知道!!7. 最后把封装成 OOP 的数据交付给 Java 业务代码使用,各种资源关闭。在编码时,只要涉及到和数据交互行为。好吧,把前面的几个步骤再走一遍。发现没有,其实你在做大量的重复工作,好好的脑力活生生变成了体力活。怎么办?难道要承受这种编程生活的折磨吗,当然不!通过模板方法解决 Jdbc 访问中的重复性问题。其实,Jdbc 编程是一个模板化的操作过程,针对不同的数据请求操作其中只有 2 个地方是不一样的。数据清单不一样。每一次、不同数据需求的 Jdbc 操作请求,SQL 语句是不一样的。另一个不一样是从关系数据库中读出来的数据封装成对应的目标对象类型是不一样的。知道这些就好办,可以把 Jdbc 代码操作封装成一个模板方法。在模板方法中预留 2 个参数:传入 SQL 语句;传入一个用于封装结果集中的数据到 OOP 对象的方法。按照这个思路,属于你的 Jdbc 简易框架就要诞生了。有点激动吧!如下是查询模板方法代码参考:/** *通用 jdbc 查询数据模板方法 *connection:连接对象,可由外部传入,也可由内部方法创建 *sql:传过来的 SQL语句 *rsh:自定义结果集处理接口,其中有封装结果集数据的方法 *args:SQL中参数值 */public <T> T query(Connection connection, String sql, ResultSetHandler<T> rsh, Object... args) throws SQLException { // 建立连接 if (connection == null) connection = this.dataSource.getConnection(); if (sql == null) throw new SQLException("SQL语句不正确"); // 预处理语句 PreparedStatement preStatement = connection.prepareStatement(sql); // SQL指定参数值 fillStatement(preStatement, args); // 结果集 ResultSet rs = preStatement.executeQuery(); // 结果处理规范 T result = rsh.handle(rs); // 资源关闭 this.close(connection, preStatement, rs); return result;}调用上面方法时,传递一条 SQL 语句,传递一个实现了 ResultSetHandler 接口的对象(此对象提供方法完成数据映射工作)就可以了。我们编写的模板方法与 Hibernate 相比较:Hibernate 会自动构建生成 SQL 语句。复杂的 SQL 也不是问题;自动完成了关系型数据库中的数据到 Java 对象的封装。所以说Hibernate 是一个全自动化的 ORM 持久化框架。当然 Hibernate 可不仅只完成数据库数据的访问,还会考虑性能、事务等生产环境中的诸多现实问题,这些会在本课程后面慢慢展开。
在说明希尔排序的整个过程之后,接下来,我们看看如何用 Java 代码实现希尔排序算法。import java.util.Arrays;public class ShellSort { public static void main(String[] args) { //初始化需要排序的数组 int array[] = {9, 2, 11, 7, 12, 5}; //初始化希尔排序的增量为数组长度 int gap = array.length; //不断地进行插入排序,直至增量为1 while (true) { //增量每次减半 gap = gap/2; for (int i = 0; i < gap; i++) { //内部循环是一个插入排序 for (int j = i + gap; j < array.length; j += gap) { int temp = array[j]; int k = j - gap; while (k >= 0 && array[k] > temp) { array[k + gap] = array[k]; k -= gap; } array[k + gap] = temp; } } //增量为1之后,希尔排序结束,退出循环 if (gap == 1) break; } //打印出排序好的序列 System.out.println(Arrays.toString(array)); }}运行结果如下:[2, 5, 7, 9, 11, 12]代码中的第 8 行初始化一个需要排序的数组,后面按照从小到大的排序规则,实现了数组的排序。第 12 行至 30 行是整个希尔排序的流程。第 14 行代码表示希尔排序中的增量每次整除 2 取得,第 17 行至 25 行是一个 for 循环结构,表明按照增量进行插入排序。最后第 32 行代码输出排序好的数组。
Java 代码相对比较简单,因为补全的结果是一个字符串数组,补全列表的列表项也都是单个项目,所以这里直接使用ArrayAdapter再好不过(关于 ArrayAdapter 的使用详见 23 节),代码如下:package com.emercy.myapplication;import android.app.Activity;import android.os.Bundle;import android.widget.ArrayAdapter;import android.widget.AutoCompleteTextView;public class MainActivity extends Activity { private AutoCompleteTextView mTextView; private String[] mDataName = {"慕课", "慕课网", "慕课Android教程", "慕斯蛋糕", "慕容复"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.autoCompleteTextView); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, mDataName); mTextView.setAdapter(adapter); }}首先我们将补全项存入字符串数组中,然后获取 AutoCompleteTextView 对象,创建 ArrayAdapter,最后为 AutoCompleteTextView 对象指定 Adapter 即可。其中在创建 ArrayAdapter 的时候我们传入了一个 id 为android.R.layout.simple_dropdown_item_1line的布局文件,它是 Android 系统为我们内置的专门用于下拉菜单使用的布局文件,其实里面只有一个 TextView 用于显示下拉菜单项,查看源码如下:<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" style="?android:attr/dropDownItemStyle" android:textAppearance="?android:attr/textAppearanceLargePopupMenu" android:singleLine="true" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:ellipsize="marquee" />我们在使用下拉菜单类型的样式时都可考虑直接采用系统样式,最终编译出来屏幕中有一个输入框,我们输入一个“慕”字,会展示以慕开头的所有可补全的字符串,结果如图所示:
在说明选择排序的整个过程之后,接下来,我们看看如何用 Java 代码实现选择排序算法。import java.util.Arrays;public class SelectSort { public static void main(String[] args) { //初始化需要排序的数组 int array[] = {9, 2, 11, 7, 12, 5}; //依次进行选择排序,每次找出最小的元素,放入待排序的序列中 for(int i=0;i<array.length;i++){ //记录最小元素min和最小元素的数组下标索引minIndex int min = array[i]; int minIndex = i; //在未排序的序列中找出最小的元素和对应数组中的位置 for(int j=i+1;j<array.length;j++){ if(array[j] < min){ min = array[j]; minIndex = j; } } //交换位置 int temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; } //打印出排序好的序列 System.out.println(Arrays.toString(array)); }}运行结果如下:[2, 5, 7, 9, 11, 12]代码中的第 7 行初始化一个需要排序的数组,后面按照从小到大的排序规则,实现了数组的排序。第 10 行是外层 for 循环,不断地重复选择排序工作。第 17 行是内层循环,不断地实现每一次 “选择 “,在未排序的序列中找出最小的元素和对应数组中的位置。第 24 至第 27 行实现了将未排序好的序列中的最小元素与需要排序的位置的元素进行交换的功能。第 31 行打印出排序好的数组。
在说明冒泡排序的整个过程之后,接下来,我们看看如何用 Java 代码实现冒泡排序算法。import java.util.Arrays;public class BubbleSort { public static void main(String[] args) { //初始化需要排序的数组 int array[] = {9,2,11,7,12,5}; //对需要排序的数组进行排序 for (int i=1; i<array.length; i++){ //针对待排序序列中除了已经排序好的元素之外,重复排序工作 for(int j=0;j<array.length-i;j++){ //当相邻两个元素需要交换时,交换相邻的两个元素 if(array[j]>array[j+1]){ int temp = array[j]; array[j] = array[j+1]; array[j+1] = temp; } } } //打印出排序好的序列 System.out.println(Arrays.toString(array)); }}运行结果如下:[2, 5, 7, 9, 11, 12]代码中的第 8 行初始化一个需要排序的数组,后面按照从小到大的排序规则,实现了数组的排序。第 11 行是外层循环,不断地重复排序工作。第 14 行是内层循环,不断地实现每一次 “冒泡” ,将最大的一个元素找出。第 17 至第 21 行实现当相邻两个元素需要交换时,交换相邻的两个元素的功能。第 25 行打印出排序好的数组。
Java 语言抽象了 java.net.DatagramSocket 类,表示一个 UDP Socket,既可以用在客户端,又可以用在服务器端。java.net.DatagramSocket 是一个包装类,对外抽象了一组方法,具体实现是在 java.net.DatagramSocketImpl 类中完成的,它允许用户自定义具体实现。java.net.DatagramSocket 类包含的主要功能如下:创建 UDP Socket,具体就是创建一个 java.net.DatagramSocket 类的对象。将 Socket 绑定到本地接口 IP 地址或者端口,可以调用 java.net.DatagramSocket 类的构造方法或 bind 方法完成。将客户端 UDP Socket 和远端 Socket 做绑定,可以通过 java.net.DatagramSocket 类的 connect 方法完成。提示:UDP 客户端调用 connect 方法,仅仅是将本地 Socket 和远端 Socket 做绑定,并不会有类似 TCP 三次握手的过程。关闭连接,可以调用 java.net.DatagramSocket 类的 close 方法完成。接收数据,可以通过 java.net.DatagramSocket 类的 receive 方法实现数据接收。发送数据,可以通过 java.net.DatagramSocket 类的 send 方法实现数据发送。java.net.Socket 类提供了一组重载的构造方法,方便程序员选择,大体分为四类:无参public DatagramSocket() throws SocketException绑定到任意可用的端口和通配符 IP 地址,比如 IPv4 的 0.0.0.0。一般用作 UDP 客户端 Socket 的创建。传入 port 参数public DatagramSocket(int port) throws SocketException绑定到由 port 指定的端口和通配符 IP 地址,比如 IPv4 的 0.0.0.0。一般用作 UDP 服务端 Socket 的创建。传入指定的 IP 和 Port 参数public DatagramSocket(SocketAddress bindaddr) throws SocketExceptionpublic DatagramSocket(int port, InetAddress laddr) throws SocketException绑定到指定的端口和指定的网络接口。如果你的主机有多个网卡,并且你指向在某个指定的网卡上收发数据,可以用此构造方法。既可以用作 UDP 客户端 Socket,也可以用作 UDP 服务端 Socket。
在 Java 5.0之前,想要定义一个枚举类较为繁琐,通常需要以下几个步骤:定义一个 Java 普通类作为枚举类,定义枚举类的属性,使用private final修饰;该类不提供外部实例化操作,因此将构造方法设置为私有,并初始化属性;在类内部,提供当前枚举类的多个对象 ,使用public static final修饰;提供常用的getter、setter或toString()方法。下面我们定义一个用于表示性别的枚举类,并演示如何调用此枚举类,其具体实例如下:/** * @author colorful@TaleLin */public class EnumDemo1 { /** * 性别枚举类 */ static class Sex { // 定义常量 private final String sexName; // 私有化构造器,不提供外部实例化 private Sex(String sexName) { // 在构造器中为属性赋值 this.sexName = sexName; } public static final Sex MALE = new Sex("男"); public static final Sex FEMALE = new Sex("女"); public static final Sex UNKNOWN = new Sex("保密"); /** * getter */ public String getSexName() { return sexName; } /** * 重写toString方法,方便外部打印调试 */ @Override public String toString() { return "Sex{" + "sexName='" + sexName + '\'' + '}'; } } public static void main(String[] args) { System.out.println(Sex.FEMALE.getSexName()); System.out.println(Sex.MALE.getSexName()); System.out.println(Sex.UNKNOWN.getSexName()); }}运行结果:女男保密
Java 抽象了 java.net.DatagramPacket 类表示一个 UDP 数据报,主要功能如下:发送:设置发送的数据。设置接收此数据的目的主机的 IP 地址和端口号。获取发送此数据的源主机的 IP 地址和端口号。接收:设置接收数据的 byte 数组。获取发送此数据的源主机的 IP 地址和端口号。获取接收此数据的主机目的主机的 IP 地址和端口号。接收数据的构造方法:public DatagramPacket(byte[] buffer, int length)public DatagramPacket(byte[] buffer, int offset, int length)当接收数据的时候,需要构造 java.net.DatagramPacket 的实例,并且要传入接收数据的 byte 数组,然后调用 java.net.DatagramSocket 的 receive 方法就可以接收数据。当 receive 方法调用返回以后,发送此数据包的源主机 IP 地址和端口号保存在 java.net.DatagramSocket 的实例中。发送数据的构造方法:public DatagramPacket(byte[] data, int length,InetAddress destination, int port)public DatagramPacket(byte[] data, int offset, int length,InetAddress destination, int port)public DatagramPacket(byte[] data, int length,SocketAddress destination)public DatagramPacket(byte[] data, int offset, int length,SocketAddress destination)当发送数据的时候,同样需要构造 java.net.DatagramPacket 的实例,并且要传入将要发送的数据的 byte 数组,同时要传入接收此数据包的目标主机 IP 地址和端口号,然后调用 java.net.DatagramSocket 的 send 方法就可以发送数据。目标主机的 IP 地址和端口号保存在 java.net.DatagramSocket 的实例中,你可以调用它的 getSocketAddress 方法获取。获取或设置数据:public byte[] getData()public void setData(byte[] data)public void setData(byte[] data, int offset, int length)获取或设置数据的长度:public int getLength()public void setLength(int length)获取设置 IP 地址和端口号public int getPort()public InetAddress getAddress() // 只能获取 IPpublic SocketAddress getSocketAddress()// 同时获取 IP 和 Portpublic void setAddress(InetAddress remote)// 只能设置 IPpublic void setPort(int port)public void setAddress(SocketAddress remote)// 设置 SocketAddress,同时设置 IP 和 Port
在 Java 中给我们提供了一个非常方便的动态代理接口 InvocationHandler,只要实现这个接口然后重写它的抽象方法 invoke()//DynamicProxy类class DynamicProxy implements InvocationHandler { private Object object;//被代理类的引用 DynamicProxy(Object object) {//传入被代理类的实例引用 this.object = object; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(object, args); }}//Client类class Client { public static void main(String[] args) { IPurchaseHouse houseOwner = new HouseOwner(); DynamicProxy dynamicProxy = new DynamicProxy(houseOwner); //Proxy.newProxyInstance方法动态构造一个代理中介,需要传入被代理类的ClassLoader、共同接口集合和dynamicProxy实例对象 IPurchaseHouse agentA = (IPurchaseHouse) Proxy.newProxyInstance(houseOwner.getClass().getClassLoader(), new Class[]{IPurchaseHouse.class}, dynamicProxy); agentA.inquiryPrice(); agentA.visitHouse(); agentA.payDeposit(); agentA.signAgreement(); agentA.payMoney(); agentA.getHouse(); }}运行结果:HouseOwner提出房子价格: 200W RMBHouseOwner同意买房者来看房子HouseOwner收了买房者1W RMB定金HouseOwner与买房者签订合同买房者付钱给HouseOwner买房者拿到房子Process finished with exit code 0
Java NIO 四个核心的组件分别是 Selector、SocketChannel、ServerSocketChannel、SelectionKey。Selector 是 I/O 事件反应器,是动力源。SocketChannel、ServerSocketChannel、SelectionKey 都是功能组件,它们之间互相配合,如下:首先创建一个 Selector 对象,然后调用它的 select 方法,进入事件等待状态。对于服务器来说,需要创建 ServerSocketChannel 对象,然后调用它的 register 方法,将 SelectionKey.OP_ACCEPT 事件注册到 Selector,准备监听新的客户端连接。如果 Selector 监听到新的客户端连接请求,SelectionKey.OP_ACCEPT 事件就会产生。调用 ServerSocketChannel 的 accept 方法,返回一个 SocketChannel 对象,需要将 SocketChannel 的 SelectionKey.OP_READ 事件注册到 Selector。在上面两步中, ServerSocketChannel 和 SocketChannel 都提供了 register 方法,返回值是 SelectionKey。SelectionKey 中绑定了上下文信息。如果 Selector 监听到 I/O 事件,它的 select 方法就会返回。可以调用 Selector 的 selectedKeys 方法,返回一个 SelectionKey 数组,包含了所有产生了 I/O 事件的 SocketChannel。遍历这个数组,逐个处理相应的 I/O 事件。
当我们的程序规模越来越大,类的数量也会随之增多,数量繁多的类会造成项目的混乱,不易于维护管理。本小节所介绍的包就是为了将类分类而产生的,我们可以使用包让程序结构更加清晰且易于管理。本小节将会学习到什么是包,如何声明包,包作用域以及包的命名规范等知识点。
java.util包下的Scanner类可用于获取用户从键盘输入的内容,我们在Java Scanner 类这一小节已经介绍过具体使用,实例如下:import java.util.Scanner;/** * @author colorful@TaleLin */public class ScannerDemo { public static void main(String[] args) { // 创建扫描器对象 Scanner scanner = new Scanner(System.in); System.out.println("请输入您的姓名:"); // 可以将用户输入的内容扫描为字符串 String name = scanner.nextLine(); // 打印输出 System.out.println("你好 ".concat(name).concat(" ,欢迎来到慕课网!")); // 关闭扫描器 scanner.close(); }}运行结果:请输入您的姓名:Colorful你好 Colorful ,欢迎来到慕课网!
在说明快速排序的整个过程之后,接下来,我们看看如何用 Java 代码实现快速排序算法。import java.util.Arrays;public class QuickSort { public static void main(String[] args) { //初始化需要排序的数组 int array[] = {9, 2, 11, 7, 12, 5}; //快速排序 quickSort(array,0,array.length-1); //打印出排序好的序列 System.out.println(Arrays.toString(array)); } //快速排序 private static void quickSort(int[] array,int low, int high){ if(low < high){ //找到分区的位置,左边右边分别进行快速排序 int index = partition(array,low,high); quickSort(array,0,index-1); quickSort(array,index+1,high); } } //快速排序分区操作 private static int partition(int[] array, int low, int high){ //选择基准 int pivot = array[low]; //当左指针小于右指针时,重复操作 while (low < high){ while(low < high && array[high] >= pivot){ high = high - 1; } array[low] = array[high]; while (low < high && array[low] <= pivot){ low = low + 1; } array[high] = array[low]; } //最后赋值基准 array[low] = pivot; //返回基准所在位置,基准位置已经排序好 return low; }}运行结果如下:[2, 5, 7, 9, 11, 12]代码中的第 8 行初始化一个需要排序的数组,后面按照从小到大的排序规则,实现了数组的排序。第 15 行到底 22 行是快速排序的外部结构,应用分治思想递归求解。代码 25 行至 43 行是分区操作,完成基于基准数据的左右分区,并将基准数据放置在排序好的位置,并且返回基准所在的位置,进行后续的分治操作。
在 Java 中在函数内部定义一个匿名内部类或者 lambda,内部类访问的函数局部变量必须需要 final 修饰,也就意味着在内部类内部或者 lambda 表达式的内部是无法去修改函数局部变量的值。可以看一个很简单的 Android 事件点击的例子:public class DemoActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_demo); final int count = 0;//需要使用final修饰 findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println(count);//在匿名OnClickListener类内部访问count必须要是final修饰 } }); }}在 Kotlin 中在函数内部定义 lambda 或者内部类,既可以访问final修饰的变量,也可以访问非 final 修饰的变量,也就意味着在 Lambda 的内部是可以直接修改函数局部变量的值。以上例子 Kotlin 实现:访问 final 修饰的变量:class Demo2Activity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_demo2) val count = 0//声明final btn_click.setOnClickListener { println(count)//访问final修饰的变量这个是和Java是保持一致的。 } }}访问非 final 修饰的变量,并修改它的值:class Demo2Activity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_demo2) var count = 0//声明非final类型 btn_click.setOnClickListener { println(count++)//直接访问和修改非final类型的变量 } }}通过以上对比会发现 Kotlin 中使用 lambda 会比 Java 中使用 lambda 更灵活,访问受到限制更少,这也就回答本博客最开始说的一句话,Kotlin 中的 lambda 表达式是真正意义上的支持闭包,而 Java中 的lambda 则不是。Kotlin 中的 lambda 表达式是怎么做到这一点的呢?
使用 @JvmOverloads 注解解决 Java 调用 Kotlin 重载函数问题:@JvmOverloads //@JvmOverloads注解fun <T> joinString( collection: Collection<T> = listOf(), separator: String = ",", prefix: String = "", postfix: String = ""): String { return collection.joinToString(separator, prefix, postfix)}//调用的地方fun main(args: Array<String>) { //函数使用命名参数可以提高代码可读性 println(joinString(collection = listOf(1, 2, 3, 4), separator = "%", prefix = "<", postfix = ">")) println(joinString(collection = listOf(1, 2, 3, 4), separator = "%", prefix = "<", postfix = ">")) println(joinString(collection = listOf(1, 2, 3, 4), prefix = "<", postfix = ">")) println(joinString(collection = listOf(1, 2, 3, 4), separator = "!", prefix = "<")) println(joinString(collection = listOf(1, 2, 3, 4), separator = "!", postfix = ">")) println(joinString(collection = listOf(1, 2, 3, 4), separator = "!")) println(joinString(collection = listOf(1, 2, 3, 4), prefix = "<")) println(joinString(collection = listOf(1, 2, 3, 4), postfix = ">")) println(joinString(collection = listOf(1, 2, 3, 4)))}Kotlin 调用 Java 不能使用命名参数和默认值参数:在 Kotlin 中函数使用命名参数即使在 Java 重载了很多构造器方法或者普通方法,在 Kotlin 中调用 Java中的方法是不能使用命名参数的,不管你是 JDK 中的函数或者是 Android 框架中的函数都是不允许使用命名参数的。
Java 中的 @Inherited 元注解介绍Inheried 顾名思义就是继承的意思,但是这里需要注意并不是表示注解类可以继承,而是如果一个父类被贴上 @Inherited 元注解标签,那么它的子类没有任何注解标签的话,这个子类就会继承来自父类的注解。类似下面的例子:@Inherited@Retention(RetentionPolicy.RUNTIME)public @interface TestAnnotation {}@TestAnnotationclass Animal { //...}class Cat extends Animal{//也会拥有来自父类Animal的@TestAnnotation注解 //...}Kotlin 为啥不需要 @Inherited 元注解?关于这个问题实际上在 Kotlin 官网的 discuss 中就有人提出了这个问题,具体感兴趣的可以去看看:Inherited annotations and other reflections enchancements。这里大概说下原因,我们都知道在 Java 中,无法找到子类方法是否重写了父类的方法。因此不能继承父类方法的注解。然而 Kotlin 目前不需要支持这个 @Inherited 元注解,因为 Kotlin 可以做到,如果反射提供了override 标记而且很容易做到。
Java 中常用的数据结构都在 java.util 包下,都是对 Collection 和 Map 两个顶级接口的实现类。这里要注意不是 java.util.Collections,Collections 是一个对集合中元素进行查询、排序等操作的工具类,我们下面还会提到。读源代码是我们最准确高效的学习手段之一,上图就是 java.util.Collection 源代码的截图,注释中红标分别列举了实现该接口的几种数据类型 List、LinkedList、ArrayList、Vector、Set、SortedSet、HashSet、TreeSet、AbstractCollection,蓝标是 Map 接口和实现该接口的 SortedMap,此外实现 Map 接口的还有 HashMap、TreeMap、Hashtable、SortedMap。另外还有 Collections、Arrays 两个工具类。从源代码中我们可以梳理出各种数据类型之间的关系,如下图:
在说明求解钢条切割问题的整个过程之后,接下来,我们看看如何用 java 代码实现钢条切割问题的求解。import java.util.Scanner;public class SteelBarCutProblem { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int[] p = {0,1,5,8,9,10,17,17,20,24,30}; int[] r = new int[p.length]; int[] s = new int[p.length]; System.out.println("请输入1到"+ (p.length-1)+"之间任意一个自然数: "); int n = scanner.nextInt(); r[0] = 0; for(int i =1; i<=n; i++){ int q = Integer.MIN_VALUE; for (int j=1; j<=i; j++){ if(q < (p[j] + r[i-j])){ q = p[j] + r[i-j]; s[i] = j; } } r[i] = q; } System.out.println("长度为"+ n +"米长的钢材最大切割收益为:"+r[n]); System.out.println("对应的具体每一段的切割长度如下:"); while (n>0){ System.out.println(s[n]); n = n - s[n]; } }运行结果如下:请输入1到10之间任意一个自然数: 8长度为8米长的钢材最大切割收益为:22对应的具体每一段的切割长度如下:26运行结果中首先需要输入一个自然数表示要切割的钢条的长度,然后对应输出该长度钢条切割之后的最大化收益以及具体的切割方法。代码中第 8 行至第 10 行分别初始化对应长度的钢材的价格表,对应长度钢条切割之后的最大化收益数组,对应长度钢条满足最大化收益时第一次切割的长度。代码的第 15 行至第 25 行主要来实现步骤 4 中的 ExtendCutSteelRod 算法,用来计算最大化的切割收益及保存解,代码的 27 行至 32 行主要是对求解结果的输出。并且代码中引用了 Scanner 类用来进行交换处理,可以在控制台输入一段需要切割的钢条长度,然后返回对应的切割结果。
在说明求解最大子数组的整个过程之后,接下来,我们看看如何用 java 代码实现最大子数组问题的求解。package divide_and_conquer;public class MaxSubarray { //内部类,用来存储最大子数组的返回结果, private static class Result { int low; int high; int sum; public Result(int low, int high, int sum) { this.low = low; this.high = high; this.sum = sum; } @Override public String toString() { return "Result{" + "low=" + low + ", high=" + high + ", sum=" + sum + '}'; } } private static Result FindMaxCrossSubarray(int[]A,int low, int mid, int high){ //寻找左边的连续最大值及记录位置 int leftSum = Integer.MIN_VALUE; int sum = 0; int maxLeft = mid; for (int i=mid; i>=low; i--){ sum = sum + A[i]; if(sum > leftSum){ leftSum = sum; maxLeft = i; } } //寻找右边的连续最大值及记录位置 int rightSum = Integer.MIN_VALUE; int maxRight = mid+1; sum = 0; for ( int j=mid+1; j<=high;j++){ sum = sum + A[j]; if(sum > rightSum){ rightSum = sum; maxRight = j; } } //返回跨越中间值的最大子数组结果 return new Result(maxLeft,maxRight,leftSum + rightSum); } public static Result FindMaxSubarray(int[] A, int low, int high){ //数组只有一个元素时的处理情况 if (high == low){ return new Result(low,high,A[low]); }else { //对应思路中步骤1,找到中间元素 int mid = (low + high)/2; //对应思路中步骤2,分别对应a,b,c三种情况求解最大子数组结果 Result leftResult = FindMaxSubarray(A,low,mid); Result rightResult = FindMaxSubarray(A,mid+1,high); Result crossResult = FindMaxCrossSubarray(A,low,mid,high); //对应步骤3,比较 if(leftResult.sum >= rightResult.sum && leftResult.sum >= crossResult.sum){ return leftResult; }else if (rightResult.sum >= leftResult.sum && rightResult.sum >= crossResult.sum){ return rightResult; }else { return crossResult; } } } public static void main(String[] args){ int[] A = {12, -3, -16, 20, -19, -3, 18, 20, -7, 12, -9, 7, -10}; System.out.println(FindMaxSubarray(A,0,A.length-1).toString()); }}运行结果如下:Result{low=6, high=9, sum=43}运行结果中的 low 表示最大子数组在数组 A 中的开始下标,high 表示最大子数组在数组 A 中的终止下标,sum 表示最大子数组的求和值,对应到我们的实例数组 A 中,对应的最大最大子数组为 [18,20,-7,12]。代码中第 5 行至 25 行的 Result 内部类,主要是用来存储最大子数组的返回结果,定义了子数组的开始下标,结束下标,求和值。代码的第 27 至 55 行是最大子数组跨越中间节点时候的最大子数组求解过程。代码的第 58 至 78 行是整个最大子数组的求解过程。代码的第 81 行和 82 行是求解最大子数组过程的一个示例,输出最大子数组的求解结果。
在说明插入排序的整个过程之后,接下来,我们看看如何用 Java 代码实现插入排序算法。import java.util.Arrays;public class InsertSort { public static void main(String[] args) { //初始化需要排序的数组 int array[] = {9, 2, 11, 7, 12, 5}; //初始化一个与待排序数组大小相同的数组,用来存放排序好的序列 int sortArray[] = new int[array.length]; //步骤1:待排序数组中选择第一个元素作为已经排序好的元素(数组的下标0表示第一个元素) sortArray[0] = array[0]; //步骤2:依次遍历未排序的元素,将其插入已排序序列中 for (int i = 1; i < array.length; i++) { //待排序元素 int temp = array[i]; //记录待排序元素需要插入已排序数组中的位置 int index = i; //从已排序好的数组右边依次遍历数组,直到找到待排序元素需要插入的位置 while( index > 0 && temp < sortArray[index-1] ){ sortArray[index] = sortArray[index-1]; index--; } //插入待排序元素 sortArray[index] = temp; } //打印出排序好的序列 System.out.println(Arrays.toString(sortArray)); }}运行结果如下:[2, 5, 7, 9, 11, 12]代码中的第 7 行初始化一个需要排序的数组,第 10 行初始化一个与待排序数组大小相同的数组,用来存放排序好的序列。第 13 行将待排序数组中选择第一个元素作为已经排序好的元素,放入排序好的数组中。第 16 行是外层循环,不断地重复排序工作,将未排序的元素插入到排序好的序列中。第 22 行是内部的 while 循环,找到待排序元素需要插入的排序好的数组中的位置,实现插入排序。第 31 行打印出排序好的数组。
在说明求解背包问题的整个过程之后,接下来,我们看看如何用 java 代码实现背包问题的求解。import java.util.ArrayList;import java.util.Collections;import java.util.List;public class Knapsack { /** * 物品内部类 */ private static class Item implements Comparable<Item>{ int type; double weight; double value; double unitValue; public Item(int type, double weight){ this.type = type; this.weight = weight; } public Item(int type, double weight,double value){ this.type = type; this.weight = weight; this.value = value; this.unitValue = value/weight; } @Override public int compareTo(Item o) { return Double.valueOf(o.unitValue).compareTo(this.unitValue); } } public static void main(String[] args){ //背包容量 double capacity = 30; //物品类型初始化数组 int[] itemType = {1,2,3,4,5}; //物品重量初始化数组 double[] itemWeight = {10,5,15,10,30}; //物品价值初始化数组 double[] itemValue = {20,30,15,25,10}; //初始化物品 List<Item> itemList = new ArrayList<>(); for(int i=0;i<itemType.length;i++){ Item item = new Item(itemType[i],itemWeight[i],itemValue[i]); itemList.add(item); } //物品按照单价降序排序 Collections.sort(itemList); //背包选择 List<Item> selectItemList = new ArrayList<>(); double selectCapacity = 0; for(Item item : itemList){ if( (selectCapacity + item.weight) <= capacity){ selectCapacity = selectCapacity + item.weight; Item selectItem = new Item(item.type,item.weight); selectItemList.add(selectItem); }else { Item selectItem = new Item(item.type, capacity-selectCapacity); selectItemList.add(selectItem); break; } } //选择结果输出 for (Item item : selectItemList){ System.out.println("选择了类型:"+ item.type+" 的物品,重量为:"+item.weight); } }}运行结果如下:选择了类型:2 的物品,重量为:5.0选择了类型:4 的物品,重量为:10.0选择了类型:1 的物品,重量为:10.0选择了类型:3 的物品,重量为:5.0代码中第 10 行至第 31 行定义了物品的一个内部类,用来存储一个物品的类型、重量、价值、单位重量的价值,并且实现在其中实现了一个对比函数。代码的第 35 至 42 行对应着开始的背包问题的初始化工作,分别初始化了背包容量、物品类型、物品重量、物品价值。代码的第 44 行至 51 行将所有物品按照物品内部类的格式加入数组,并且按照物品单位重量的价值进行降序排序。代码的第 53 行至第 66 行,按照背包问题的贪心选择方法选择对应的物品,并记录选择的物品类型及重量,放入到选择的物品列表中 ,代码的 69 行 71 行输出相关的物品选择结果。
有了上述关于类型本质的阐述,我们一起来看下 Java 中的一些基本类型来套用类型本质的定义,来看看有什么问题。使用类型的定义验证int类型例如一个 int 类型的变量,那么表明它只能存储 int 类型的数据,我们都知道它用4个字节存储,数值表示范围是-2147483648 ~ 2147483647,那么规定该类型可能存在的值,然后我们可以对该类型的值进行运算操作。似乎没毛病,int类型和类型本质阐述契合的是如此完美。但是String类型呢?也是这样的吗?请接着往下看:使用类型的定义验证String类型或其他定义类对应的类型例如一个 String 类型的变量,在 Java 中它却可以存在两种值:一个是String类的实例另一种则是null。然后我们可以对这些值进行一些操作,第一种String类实例当然允许你调用String类所有操作方法,但是对于第二种null值,操作则非常有限,如果你强行使用null值去操作String类中的操作方法,那么恭喜你,你将获得一个NullPointerException空指针异常。在 Java 中为了程序的健壮性,这就要求开发者对 String 类型的值还得需要做额外的判断,然后再做相应的处理,如果不做额外判断处理那么就很容易得到空指针异常。这就出现同一种类型变量存在多种值,却不能得到平等一致的对待。对比上述 int 类型的存在的值都是一致对待,所有该类型上所有可能的值都可以进行相同的运算操作。下面接着看着一个很有趣例子:貌似连 Java 中的instanceof都不承认null是一个String类型的值。这两种值的操作也完全不一样:真实的String允许你调用它的任何方法,而null值只允许非常有限的操作。那么 Kotlin 类型系统是如何解决这样的问题的呢? 请接着往下看。
一直以来,我们都使用System.out.println()方法向屏幕打印内容,那么如何接收输入的内容呢?本小节所学习的Scanner类就可以实现对输入内容的接收。在本小节,我们将学习Scanner类的定义,如何使用Scanner类以及其常用方法,在学完这些基础知识后,我们会在最后学习一个比较有趣的实例程序。