深入理解Java的動態編譯

前提

筆者很久之前就有個想法:參考現有的主流ORM框架的設計,造一個ORM輪子,在基本不改變使用體驗的前提下把框架依賴的大量的反射設計去掉,這些反射API構築的組件使用動態編譯加載的實例去替代,從而可以得到接近於直接使用原生JDBC的性能。於是帶着這樣的想法,深入學習Java的動態編譯。編寫本文的時候使用的是JDK11

基本原理

下面這個很眼熟的圖來源於《深入理解Java虛擬機》前端編譯與優化的章節,主要描述編譯的過程:

上圖看起來只有三步,其實每一步都有大量的步驟,下圖嘗試相對詳細地描述具體的步驟(圖比較大難以分割,直接放原圖):

實際上,僅僅對於編譯這個過程來說,開發者或者使用者不必要完全掌握其中的細節,JDK提供了一個工具包javax.tools讓使用者可以用簡易的API進行編譯(其實在大多數請下,開發者是面向業務功能開發,像編譯和打包這些細節一般直接由開發工具、MavenGradle等工具完成):

具體的使用過程包括:

  • 獲取一個javax.tools.JavaCompiler實例。
  • 基於Java文件對象初始化一個編譯任務javax.tools.JavaCompiler$CompilationTask實例。
  • CompilationTask實例執行結果代表着編譯過程的成功與否。

我們熟知的javac編譯器其實就是JavaCompiler接口的實現,在JDK11中,對應的實現類為com.sun.tools.javac.api.JavacTool。在JDK8中不存在JavaCompiler接口,具體的編譯入口類為com.sun.tools.javac.main.JavaCompiler

因為JVM裏面的Class是基於ClassLoader隔離的,所以編譯成功之後可以通過自定義的類加載器加載對應的類實例,然後就可以應用反射API進行實例化和後續的調用。

JDK動態編譯

JDK動態編譯的步驟在上一節已經清楚地說明,這裏造一個簡單的場景。假設存在一個接口如下:

package club.throwable.compile;

public interface HelloService {
    
    void sayHello(String name);
}

// 默認實現
package club.throwable.compile;

public class DefaultHelloService implements HelloService {

    @Override
    public void sayHello(String name) {
        System.out.println(String.format("%s say hello [by default]", name));
    }
}

我們可以通過字符串SOURCE_CODE定義一個類:

static String SOURCE_CODE = "package club.throwable.compile;\n" +
        "\n" +
        "public class JdkDynamicCompileHelloService implements HelloService{\n" +
        "\n" +
        "    @Override\n" +
        "    public void sayHello(String name) {\n" +
        "        System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" +
        "    }\n" +
        "}";

// 這裏不需要定義類文件,還原類文件內容如下
package club.throwable.compile;

public class JdkDynamicCompileHelloService implements HelloService{

    @Override
    public void sayHello(String name) {
        System.out.println(String.format("%s say hello [by jdk dynamic compile]", name));
    }
}

在組裝編譯任務實例之前,還有幾項工作需要完成:

  • 內置的JavaFileObject標準實現SimpleJavaFileObject是面向類源碼文件,由於動態編譯時候輸入的是類源碼文件的內容字符串,需要自行實現JavaFileObject
  • 內置的JavaFileManager是面向類路徑下的Java源碼文件進行加載,這裏也需要自行實現JavaFileManager
  • 需要自定義一個ClassLoader實例去加載編譯出來的動態類。

實現JavaFileObject

自行實現一個JavaFileObject,其實可以簡單點直接繼承SimpleJavaFileObject,覆蓋需要用到的方法即可:

public class CharSequenceJavaFileObject extends SimpleJavaFileObject {

    public static final String CLASS_EXTENSION = ".class";

    public static final String JAVA_EXTENSION = ".java";

    private static URI fromClassName(String className) {
        try {
            return new URI(className);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(className, e);
        }
    }

    private ByteArrayOutputStream byteCode;
    private final CharSequence sourceCode;

    public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
        super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
        this.sourceCode = sourceCode;
    }

    public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
        super(fromClassName(fullClassName), kind);
        this.sourceCode = null;
    }

    public CharSequenceJavaFileObject(URI uri, Kind kind) {
        super(uri, kind);
        this.sourceCode = null;
    }
    
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return sourceCode;
    }

    @Override
    public InputStream openInputStream() {
        return new ByteArrayInputStream(getByteCode());
    }
    
    // 注意這個方法是編譯結果回調的OutputStream,回調成功后就能通過下面的getByteCode()方法獲取目標類編譯后的字節碼字節數組
    @Override
    public OutputStream openOutputStream() {
        return byteCode = new ByteArrayOutputStream();
    }

    public byte[] getByteCode() {
        return byteCode.toByteArray();
    }
}

如果編譯成功之後,直接通過自行添加的CharSequenceJavaFileObject#getByteCode()方法即可獲取目標類編譯后的字節碼對應的字節數組(二進制內容)。這裏的CharSequenceJavaFileObject預留了多個構造函數用於兼容原有的編譯方式。

實現ClassLoader

只要簡單繼承ClassLoader即可,關鍵是要覆蓋原來的ClassLoader#findClass()方法,用於搜索自定義的JavaFileObject實例,從而提取對應的字節碼字節數組進行裝載,為了實現這一點可以添加一個哈希表作為緩存,鍵-值分別是全類名的別名(xx.yy.MyClass形式,而非URI模式)和目標類對應的JavaFileObject實例。

public class JdkDynamicCompileClassLoader extends ClassLoader {

    public static final String CLASS_EXTENSION = ".class";

    private final Map<String, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap();

    public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
        super(parentClassLoader);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        JavaFileObject javaFileObject = javaFileObjectMap.get(name);
        if (null != javaFileObject) {
            CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
            byte[] byteCode = charSequenceJavaFileObject.getByteCode();
            return defineClass(name, byteCode, 0, byteCode.length);
        }
        return super.findClass(name);
    }

    @Nullable
    @Override
    public InputStream getResourceAsStream(String name) {
        if (name.endsWith(CLASS_EXTENSION)) {
            String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
            CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
            if (null != javaFileObject && null != javaFileObject.getByteCode()) {
                return new ByteArrayInputStream(javaFileObject.getByteCode());
            }
        }
        return super.getResourceAsStream(name);
    }

    /**
     * 暫時存放編譯的源文件對象,key為全類名的別名(非URI模式),如club.throwable.compile.HelloService
     */
    void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
        javaFileObjectMap.put(qualifiedClassName, javaFileObject);
    }

    Collection<JavaFileObject> listJavaFileObject() {
        return Collections.unmodifiableCollection(javaFileObjectMap.values());
    }
}

實現JavaFileManager

JavaFileManagerJava文件的抽象管理器,它用於管理常規的Java文件,但是不局限於文件,也可以管理其他來源的Java類文件數據。下面就通過實現一個自定義的JavaFileManager用於管理字符串類型的源代碼。為了簡單起見,可以直接繼承已經存在的ForwardingJavaFileManager

public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private final JdkDynamicCompileClassLoader classLoader;
    private final Map<URI, JavaFileObject> javaFileObjectMap = Maps.newConcurrentMap();

    public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
        super(fileManager);
        this.classLoader = classLoader;
    }

    private static URI fromLocation(Location location, String packageName, String relativeName) {
        try {
            return new URI(location.getName() + '/' + packageName + '/' + relativeName);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
        JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
        if (null != javaFileObject) {
            return javaFileObject;
        }
        return super.getFileForInput(location, packageName, relativeName);
    }

    /**
     * 這裡是編譯器返回的同(源)Java文件對象,替換為CharSequenceJavaFileObject實現
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
        classLoader.addJavaFileObject(className, javaFileObject);
        return javaFileObject;
    }

    /**
     * 這裏覆蓋原來的類加載器
     */
    @Override
    public ClassLoader getClassLoader(Location location) {
        return classLoader;
    }

    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
        if (file instanceof CharSequenceJavaFileObject) {
            return file.getName();
        }
        return super.inferBinaryName(location, file);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
        Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
        List<JavaFileObject> result = Lists.newArrayList();
        // 這裏要區分編譯的Location以及編譯的Kind
        if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
            // .class文件以及classPath下
            for (JavaFileObject file : javaFileObjectMap.values()) {
                if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
                    result.add(file);
                }
            }
            // 這裏需要額外添加類加載器加載的所有Java文件對象
            result.addAll(classLoader.listJavaFileObject());
        } else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
            // .java文件以及編譯路徑下
            for (JavaFileObject file : javaFileObjectMap.values()) {
                if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
                    result.add(file);
                }
            }
        }
        for (JavaFileObject javaFileObject : superResult) {
            result.add(javaFileObject);
        }
        return result;
    }

    /**
     * 自定義方法,用於添加和緩存待編譯的源文件對象
     */
    public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
        javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
    }
}

注意在這個類中引入了自定義類加載器JdkDynamicCompileClassLoader,目的是為了實現JavaFileObject實例的共享以及為文件管理器提供類加載器實例。

動態編譯和運行

前置準備工作完成,我們可以通過JavaCompiler去編譯這個前面提到的字符串,為了字節碼的兼容性更好,編譯的時候可以指定稍低的JDK版本例如1.6

public class Client {

    static String SOURCE_CODE = "package club.throwable.compile;\n" +
            "\n" +
            "public class JdkDynamicCompileHelloService implements HelloService{\n" +
            "\n" +
            "    @Override\n" +
            "    public void sayHello(String name) {\n" +
            "        System.out.println(String.format(\"%s say hello [by jdk dynamic compile]\", name));\n" +
            "    }\n" +
            "}";

    /**
     * 編譯診斷收集器
     */
    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    public static void main(String[] args) throws Exception {
        // 獲取系統編譯器實例
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 設置編譯參數 - 指定編譯版本為JDK1.6以提高兼容性
        List<String> options = new ArrayList<>();
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");
        // 獲取標準的Java文件管理器實例
        StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
        // 初始化自定義類加載器
        JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
        // 初始化自定義Java文件管理器實例
        JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
        String packageName = "club.throwable.compile";
        String className = "JdkDynamicCompileHelloService";
        String qualifiedName = packageName + "." + className;
        // 構建Java源文件實例
        CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, SOURCE_CODE);
        // 添加Java源文件實例到自定義Java文件管理器實例中
        fileManager.addJavaFileObject(
                StandardLocation.SOURCE_PATH,
                packageName,
                className + CharSequenceJavaFileObject.JAVA_EXTENSION,
                javaFileObject
        );
        // 初始化一個編譯任務實例
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(
                null,
                fileManager,
                DIAGNOSTIC_COLLECTOR,
                options,
                null,
                Lists.newArrayList(javaFileObject)
        );
        // 執行編譯任務
        Boolean result = compilationTask.call();
        System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
        Class<?> klass = classLoader.loadClass(qualifiedName);
        HelloService instance = (HelloService) klass.getDeclaredConstructor().newInstance();
        instance.sayHello("throwable");
    }
}

輸出結果如下:

編譯[club.throwable.compile.JdkDynamicCompileHelloService]結果:true
throwable say hello [by jdk dynamic compile]

可見通過了字符串的類源碼,實現了動態編譯、類加載、反射實例化以及最終的方法調用。另外,編譯過程的診斷信息可以通過DiagnosticCollector實例獲取。為了復用,這裏可以把JDK動態編譯的過程抽取到一個方法中:

public final class JdkCompiler {

    static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();

    @SuppressWarnings("unchecked")
    public static <T> T compile(String packageName,
                                String className,
                                String sourceCode,
                                Class<?>[] constructorParamTypes,
                                Object[] constructorParams) throws Exception {
        // 獲取系統編譯器實例
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 設置編譯參數
        List<String> options = new ArrayList<>();
        options.add("-source");
        options.add("1.6");
        options.add("-target");
        options.add("1.6");
        // 獲取標準的Java文件管理器實例
        StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
        // 初始化自定義類加載器
        JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
        // 初始化自定義Java文件管理器實例
        JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
        String qualifiedName = packageName + "." + className;
        // 構建Java源文件實例
        CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
        // 添加Java源文件實例到自定義Java文件管理器實例中
        fileManager.addJavaFileObject(
                StandardLocation.SOURCE_PATH,
                packageName,
                className + CharSequenceJavaFileObject.JAVA_EXTENSION,
                javaFileObject
        );
        // 初始化一個編譯任務實例
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(
                null,
                fileManager,
                DIAGNOSTIC_COLLECTOR,
                options,
                null,
                Lists.newArrayList(javaFileObject)
        );
        Boolean result = compilationTask.call();
        System.out.println(String.format("編譯[%s]結果:%s", qualifiedName, result));
        Class<?> klass = classLoader.loadClass(qualifiedName);
        return (T) klass.getDeclaredConstructor(constructorParamTypes).newInstance(constructorParams);
    }
}

Javassist動態編譯

既然有JDK的動態編譯,為什麼還存在Javassist這樣的字節碼增強工具?撇開性能或者效率層面,JDK動態編譯存在比較大的局限性,比較明顯的一點就是無法完成字節碼插樁,換言之就是無法基於原有的類和方法進行修飾或者增強,但是Javassist可以做到。再者,Javassist提供的APIJDK反射的API十分相近,如果反射平時用得比較熟練,Javassist的上手也就變得比較簡單。這裏僅僅列舉一個增強前面提到的DefaultHelloService的例子,先引入依賴:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>

編碼如下:

public class JavassistClient {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("club.throwable.compile.DefaultHelloService");
        CtMethod ctMethod = cc.getDeclaredMethod("sayHello", new CtClass[]{pool.get("java.lang.String")});
        ctMethod.insertBefore("System.out.println(\"insert before by Javassist\");");
        ctMethod.insertAfter("System.out.println(\"insert after by Javassist\");");
        Class<?> klass = cc.toClass();
        System.out.println(klass.getName());
        HelloService helloService = (HelloService) klass.getDeclaredConstructor().newInstance();
        helloService.sayHello("throwable");
    }
}

輸出結果如下:

club.throwable.compile.DefaultHelloService
insert before by Javassist
throwable say hello [by default]
insert after by Javassist

Javaassist這個單詞其實是JavaAssist兩個單詞拼接在一起,意為Java助手,是一個Java字節碼增強類庫:

  • 可以基於已經存在的類進行字節碼增強,例如修改已經存在的方法、變量,甚至是直接在原有的類中添加新的方法等。
  • 可以完全像積木拼接一樣,動態拼出一個全新的類。

不像ASMASM的學習曲線比較陡峭,屬於相對底層的字節碼操作類庫,當然從性能上來看ASM對字節碼增強的效率遠高於其他高層次封裝的框架)那樣需要對字節碼編程十分了解,Javaassist降低了字節碼增強功能的入門難度。

進階例子

現在定義一個接口MysqlInfoMapper,用於動態執行一條已知的SQL,很簡單,就是查詢MySQL的系統表mysql裏面的用戶信息SELECT Host,User FROM mysql.user

@Data
public class MysqlUser {

    private String host;
    private String user;
}

public interface MysqlInfoMapper {

    List<MysqlUser> selectAllMysqlUsers();
}

假設現在只提供一個MySQL的驅動包(mysql:mysql-connector-java:jar:8.0.20),暫時不能依賴任何高層次的框架,要動態實現MysqlInfoMapper接口,優先整理需要的組件:

  • 需要一個連接管理器去管理MySQL的連接。
  • 需要一個SQL執行器用於執行查詢SQL
  • 需要一個結果處理器去提取和轉換查詢結果。

為了簡單起見,筆者在定義這三個組件接口的時候順便在接口中通過單例進行實現(部分配置完全寫死):

// 連接管理器
public interface ConnectionManager {

    String USER_NAME = "root";

    String PASS_WORD = "root";

    String URL = "jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false";

    Connection newConnection() throws SQLException;

    void closeConnection(Connection connection);

    ConnectionManager X = new ConnectionManager() {

        @Override
        public Connection newConnection() throws SQLException {
            return DriverManager.getConnection(URL, USER_NAME, PASS_WORD);
        }

        @Override
        public void closeConnection(Connection connection) {
            try {
                connection.close();
            } catch (Exception ignore) {

            }
        }
    };
}

// 執行器
public interface SqlExecutor {

    ResultSet execute(Connection connection, String sql) throws SQLException;

    SqlExecutor X = new SqlExecutor() {

        @Override
        public ResultSet execute(Connection connection, String sql) throws SQLException {
            Statement statement = connection.createStatement();
            statement.execute(sql);
            return statement.getResultSet();
        }
    };
}

// 結果處理器
public interface ResultHandler<T> {

    T handleResultSet(ResultSet resultSet) throws SQLException;

    ResultHandler<List<MysqlUser>> X = new ResultHandler<List<MysqlUser>>() {
        @Override
        public List<MysqlUser> handleResultSet(ResultSet resultSet) throws SQLException {
            try {
                List<MysqlUser> result = Lists.newArrayList();
                while (resultSet.next()) {
                    MysqlUser item = new MysqlUser();
                    item.setHost(resultSet.getString("Host"));
                    item.setUser(resultSet.getString("User"));
                    result.add(item);
                }
                return result;
            } finally {
                resultSet.close();
            }
        }
    };
}

接着需要動態編譯MysqlInfoMapper的實現類,它的源文件的字符串內容如下(注意不要在類路徑下新建這個DefaultMysqlInfoMapper類):

package club.throwable.compile;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.List;

public class DefaultMysqlInfoMapper implements MysqlInfoMapper {

    private final ConnectionManager connectionManager;
    private final SqlExecutor sqlExecutor;
    private final ResultHandler resultHandler;
    private final String sql;

    public DefaultMysqlInfoMapper(ConnectionManager connectionManager,
                                  SqlExecutor sqlExecutor,
                                  ResultHandler resultHandler,
                                  String sql) {
        this.connectionManager = connectionManager;
        this.sqlExecutor = sqlExecutor;
        this.resultHandler = resultHandler;
        this.sql = sql;
    }

    @Override
    public List<MysqlUser> selectAllMysqlUsers() {
        try {
            Connection connection = connectionManager.newConnection();
            try {
                ResultSet resultSet = sqlExecutor.execute(connection, sql);
                return (List<MysqlUser>) resultHandler.handleResultSet(resultSet);
            } finally {
                connectionManager.closeConnection(connection);
            }
        } catch (Exception e) {
            // 暫時忽略異常處理,統一封裝為IllegalStateException
            throw new IllegalStateException(e);
        }
    }
}

然後編寫一個客戶端進行動態編譯和執行:

public class MysqlInfoClient {

    static String SOURCE_CODE = "package club.throwable.compile;\n" +
            "import java.sql.Connection;\n" +
            "import java.sql.ResultSet;\n" +
            "import java.util.List;\n" +
            "\n" +
            "public class DefaultMysqlInfoMapper implements MysqlInfoMapper {\n" +
            "\n" +
            "    private final ConnectionManager connectionManager;\n" +
            "    private final SqlExecutor sqlExecutor;\n" +
            "    private final ResultHandler resultHandler;\n" +
            "    private final String sql;\n" +
            "\n" +
            "    public DefaultMysqlInfoMapper(ConnectionManager connectionManager,\n" +
            "                                  SqlExecutor sqlExecutor,\n" +
            "                                  ResultHandler resultHandler,\n" +
            "                                  String sql) {\n" +
            "        this.connectionManager = connectionManager;\n" +
            "        this.sqlExecutor = sqlExecutor;\n" +
            "        this.resultHandler = resultHandler;\n" +
            "        this.sql = sql;\n" +
            "    }\n" +
            "\n" +
            "    @Override\n" +
            "    public List<MysqlUser> selectAllMysqlUsers() {\n" +
            "        try {\n" +
            "            Connection connection = connectionManager.newConnection();\n" +
            "            try {\n" +
            "                ResultSet resultSet = sqlExecutor.execute(connection, sql);\n" +
            "                return (List<MysqlUser>) resultHandler.handleResultSet(resultSet);\n" +
            "            } finally {\n" +
            "                connectionManager.closeConnection(connection);\n" +
            "            }\n" +
            "        } catch (Exception e) {\n" +
            "            // 暫時忽略異常處理,統一封裝為IllegalStateException\n" +
            "            throw new IllegalStateException(e);\n" +
            "        }\n" +
            "    }\n" +
            "}\n";

    static String SQL = "SELECT Host,User FROM mysql.user";

    public static void main(String[] args) throws Exception {
        MysqlInfoMapper mysqlInfoMapper = JdkCompiler.compile(
                "club.throwable.compile",
                "DefaultMysqlInfoMapper",
                SOURCE_CODE,
                new Class[]{ConnectionManager.class, SqlExecutor.class, ResultHandler.class, String.class},
                new Object[]{ConnectionManager.X, SqlExecutor.X, ResultHandler.X, SQL});
        System.out.println(JSON.toJSONString(mysqlInfoMapper.selectAllMysqlUsers()));
    }
}

最終的輸出結果是:

編譯[club.throwable.compile.DefaultMysqlInfoMapper]結果:true
[{"host":"%","user":"canal"},{"host":"%","user":"doge"},{"host":"localhost","user":"mysql.infoschema"},{"host":"localhost","user":"mysql.session"},{"host":"localhost","user":"mysql.sys"},{"host":"localhost","user":"root"}]

然後筆者查看本地安裝的MySQL中的結果,驗證該查詢結果是正確的。

這裏筆者為了簡化整個例子,沒有在MysqlInfoMapper#selectAllMysqlUsers()方法中添加查詢參數,可以嘗試一下查詢的SQLSELECT Host,User FROM mysql.user WHERE User = 'xxx'場景下的編碼實現。

如果把動態實現的DefaultMysqlInfoMapper註冊到IOC容器中,就可以實現MysqlInfoMapper按照類型自動裝配。
如果把SQL和參數處理可以抽離到單獨的文件中,並且實現一個對應的文件解析器,那麼就可以把類文件和SQL隔離,MybatisHibernate都是這樣做的。

小結

動態編譯或者更底層的面向字節碼層面的編程,其實是一個十分有挑戰性但是可以創造無限可能的領域,本文只是簡單分析了一下Java源碼編譯的過程,並且通過一些簡單的例子進行動態編譯的模擬,離使用於實際應用中還有不少距離,後面需要花更多的時間去分析一下相關領域的知識。

參考資料:

  • JDK11部分源碼
  • 《深入理解Java虛擬機 – 3rd》
  • Javassist

(本文完 c-4-d e-a-20200606 0:23)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

去擺攤吧,落魄的Java程序員

真的,我也打算去擺攤,宣傳語我都想好了。沉默王二,一枚有顏值卻靠才華苟且的程序員,《Web 全棧開發進階之路》作者,CSDN 明星博主,周排名第 4,總排名 40,這數據在眾多互聯網大咖面前不值一提,但在洛陽,我想還是有一席之地的。

況且我家裡有很多書,每天晚上帶上二三十本書,到河科大學校門口擺個攤,前十名免費送,後面的書,價格隨意,只要同學們能夠負擔得起,隨意,一塊,一毛都可以,能拉動點經濟是點,也算是做出貢獻了。

另外,我還附送上我的個人微信,這個價值比書還要值錢得多,對吧?加了我的微信,同學們可以隨時隨地找我提問,還可以第一時間從我的朋友圈收到各種有趣有益的消息,真的超值啊。

我這算是积極響應國家號召了,對吧?總理都點贊好幾次地攤經濟了,朋友圈和微信社群都刷爆了。有一段白岩松老師的話,我覺得挺經典的,分享出來,大家感受一下。

我不喜歡地攤這個詞,應該叫室外經營,或者有序佔道經營,只要地方政府能夠放得開,就一定能夠拉動商戶的經營狀況。

連我老婆都坐不住了,強烈建議我去擺地攤,並且願意下班后和我一起,不嫌丟人——真患難與共啊。宣傳文案她已經幫我打印好了,今天晚上我們一家三口(帶上女兒)就去大學門口體驗一下,之前從未有過這方面的經驗,一想到這,內心竟然有些小激動。

本來,擺攤在我心目中是一種挺 low 的行為,要拋頭露面,要使勁的吆喝,還要被城管追着屁股跑,實在是狼狽。但現在我改變看法了,覺得擺攤不僅接地氣,說不定真能體驗出不一樣的生活樂趣。掙不掙錢是小事,重在參与,重在振興城市經濟。

況且,生活實在是太難了,必須得做出點改變了。就拿我來說吧,公眾號的亂序讓文章的打開率下降到了 4% 左右,之前是 8% 左右,四月底那會真的是信心滿滿,現在基本上一半的打開率沒有了,搞得挺焦慮的。

讀者訂閱數在增加,但閱讀量在下降,微信這波操作挺讓我心碎的。雖然說,面對短視頻的衝擊,圖文的整體閱讀在下降,微信不得不做出改變。但這次改變,我顯然不是受益者。

想想也挺悲哀的。所有的作者都拼了命的,從外部引流到公眾號,結果公眾號學起了今日頭條,強行加了推薦算法。這意味着什麼?

作者不再是公號的主人,讀者不再是公號的客人,中間多了一層皮條客,他願意撮合你倆,你倆就能見面,不願意的話,哼,門都沒有。

這對於讀者訂閱數龐大的號來說,閱讀量根本就不會受到影響,對吧,反正這個讀者看不到,另外一個讀者能看得到。

有一小部分讀者應該知道,我還有一個小號,“沉默王三”,已經有一段時間沒有更新了,原因很簡單,讀者訂閱數出現了負增長,所以我就喪失了更新的動力。

你看,連我這種有一些讀者基數的作者都養不動一個小號,更何況那些真正零起步的作者——太難了,還不如想想辦法去擺攤吧,不,還是好好乾自己的本職工作吧。

幸好“沉默王二”這個號的讀者增長還算是不那麼令人失望,否則真的有點坐不住了。面對這種困難的局面,我所能做的就是堅持初心,擁抱變化。

我寫作的初心是什麼?就是為了分享自己的心聲,自己的故事,自己對技術的一些理解,對人生的一些思考,給需要的讀者一丟丟幫助。

也許之前一個讀者的留言是對的,我不應該過多的關注閱讀量,更要注重文章的質量。總之,先佛。

我應該做出哪些改變呢?擺攤算是一種吧。更概括性的說法,就是,把自己寫作的主題與社會的熱點貼近一些,同時,親身去體會一些從前從未嘗試過的事情。這不僅能夠讓我的臉皮更厚一些,也能讓我多接觸一些新鮮的事情,從生活中尋找寫作的靈感。

堅持和變化,兩者相輔相成,我想一定能夠幫助我渡過難關,我是有這種信心的。

下面這段話是我在網上看到的,我覺得挺符合我現在的心境的:

生活總要嘗試不同的風景,人生總要嘗試不同的體驗。就好比旅遊,不就是從一個自己待煩的地方去另一個別人待煩的地方嗎?

時隔多年後,究竟會怎樣,不重要,重要的是經歷。起起伏伏才是人生,平淡無奇才最無聊。我去擺攤,追求的不是利潤,而是生活的體驗。

如果這次賣書能夠大獲成功的話,我還有很多才藝可以就地販賣,比如說裝機、賣假髮、賣格子衫,對了,我精通 Java,沒有對象的同學,我可以幫你 new 一個。

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。回復關鍵字「簡歷」更有一份技術大佬整理的優質簡歷模板,助你一臂之力。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

搞清楚C語言指針

Part 0:為什麼要寫這篇文章

C語言中的指針是C語言的精髓,也是C語言的重難點之一。
然而,很少有教程能把指針講的初學者能聽懂,還不會引起歧義。
本文章會嘗試做到這一點,如有錯誤,請指出。

Part 1:地址和&

我們先拋開指針不談,來講一個小故事:

一天,小L準備去找小S玩。但是小L不知道小S的家住在哪裡,正當他着急的時候,他看到了一個路牌,上面寫着:小S的家在神仙小區403

哦,真的是要素過多。為什麼這麼說?

  1. 小L和小S:我們可以看做是兩個變量/常量。
  2. 小S的家:這裏可以看做是變量/常量小S的地址。
    我們要搞清楚,每個變量/常量都和我們一樣:我們每個人都有自己的家,正如變量也有自己的地址。通俗的理解,地址是給變量/常量來存放值的地點
  3. 路牌:注意注意注意!這裏就指出了變量/常量小S的地址:神仙小區403
    事實上,我們等會會講,輸出一個變量的地址其實是個16進制的数字。

搞懂了上面,我們再來聊聊&
&這個符號我們一個不陌生,你最初用到應該是在:scanf("%d",&a)裡邊。
&叫做取址符,用來獲取一個變量/常量的地址。
那麼我們為什麼要在scanf裡邊用&,不在printf裡邊用呢?
一開始我也很疑惑,後來我看到了這個例子:
你是一個新生,你要進教室。
但是你並不知道教室在哪裡,這個時候你需要教室的地址。
下課了,你要出教室。
由於你已經在教室里了,你就不需要獲取教室的地址就可以出去了。

Part 2:一定要記住的東西

一定要記住:指針就是個變量!
重要的事情說三次:
指針就是個變量!他儲存的是地址!他自己也有地址!
指針就是個變量!他儲存的是地址!他自己也有地址!
指針就是個變量!他儲存的是地址!他自己也有地址!

為什麼這麼說?我們從指針的定義開始:

指針的定義方法:<類型名+*> [名稱]
也就是說,指針的定義大概是這樣的:

int* ip;            //類型是int*,名稱是ip
float* fp;          //類型是float*,名稱是fp
double* dp;         //類型是double*,名稱是dp

有的書上會這麼寫:

int *ip;
float *fp;
double *dp;

這麼寫當然沒問題,但是對於初學者來說,有兩個問題:

  1. 有的初學者會把*p當做是指針名
  2. 有的初學者會把定義時出現的*p取值時出現的*p弄混

指針他有沒有值?有!我們會在下一節給他賦值。
既然他的定義方式和變量一樣,他也有值,他為什麼不是變量呢?

Part 3:與指針相關的幾個符號

與指針相關的符號有兩個,一個是&,一個是*
先來聊聊&
&我們上面講過,他是來取地址的。舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    float b = 10.3;
    printf("%p,%p",&a,&b);
}

%p用來輸出地址,當然,你也可以寫成%d或者%x。先不管這個,我們來看看他會輸出什麼:

那麼也就是說,變量ab的地址是000000000062FE1C000000000062FE18
那麼我們怎麼把這個地址給指針呢?很簡單:p = &a;,舉個例子:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("a的地址:%p\n",&a);
    printf("指針p自身的地址:%p\n",&p);
    printf("指針p指向的地址:%p",p);
}

得到輸出:

a的地址:000000000062FE1C
指針p自身的地址:000000000062FE10
指針p指向的地址:000000000062FE1C

你發現了嗎?如果我們有p = &a;,我們發現:直接輸出p會輸出a的地址,輸出&p會輸出p的地址(這就是為什麼我一再強調p是個變量,他有自己的地址,正如路牌上有地址,路牌自身也有個地址一樣)。

請注意!如果你的指針為int*,那麼你只能指向int類型;如果是double*類型,只能指向double類型,以此類推

當然,void*類型的指針可以轉化為任何一種不同的指針類型(如int*,double*等等)

那麼,我們來聊聊第二個符號*
*有兩個用法。第一個在定義指針時用到,第二個則是取值,什麼意思?看下面這個例子:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("a的地址:%p\n",&a);
    printf("指針p自身的地址:%p\n",&p);
    printf("指針p指向的地址:%p\n",p);
    printf("指針p指向的地址的值:%d",*p);
}

得到輸出:

a的地址:000000000062FE1C
指針p自身的地址:000000000062FE10
指針p指向的地址:000000000062FE1C
指針p指向的地址的值:10

哈,我們得到了a的值!
也就是說,當我們有p = &a,我們可以用*p得到a的值。
那能不能操作呢?當然可以。
我們可以把*p當做a的值,那麼,我們嘗試如下代碼:

#include <stdio.h>
int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("指針p指向的地址的值:%d\n",*p);
    *p = 13;
    printf("指針p指向的地址的值:%d\n",*p);
    *p += 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p -= 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p *= 9;
    printf("指針p指向的地址的值:%d\n",*p);
    *p /= 3;
    printf("指針p指向的地址的值:%d\n",*p);
    *p %= 3;
    printf("指針p指向的地址的值:%d\n",*p);
}

得到輸出:

指針p指向的地址的值:10
指針p指向的地址的值:13
指針p指向的地址的值:16
指針p指向的地址的值:13
指針p指向的地址的值:117
指針p指向的地址的值:39
指針p指向的地址的值:0

棒極了!我們可以用指針來操作變量了。
那麼,我們要這個干什麼用呢?請看下一節:實現交換函數

Part 4:交換函數

交換函數是指針必學的一個東西。一般的交換我們會這麼寫:

t = a;
a = b;
b = t;

那麼我們把它塞到函數裡邊:

void swap(int a,int b){
      int t;
      t = a;
      a = b;
      b = t;
}

好,我們滿懷信心的調用他:

#include <stdio.h>
void swap(int a,int b){
      int t;
      t = a;
      a = b;
      b = t;
}
int main(){
      int x = 5,y = 10;
      printf("x=%d,y=%d\n",x,y);
      swap(x,y);
      printf("x=%d,y=%d",x,y);
}

於是乎,你得到了這個輸出:

x=5,y=10
x=5,y=10

啊啊啊啊啊啊啊啊,為什麼不行!!!
問題就在你的swap函數,我們來看看他們做了些啥:

swap(x,y);             --->把x賦值給a,把y賦值給b
///進入函數體
int t;                 --->定義t
t = a;                 --->t賦值為a
a = b;                 --->a賦值為b
b = t;                 --->b賦值為t

各位同學,函數體內有任何一點談到了x和y嗎?
所謂的交換,交換的到底是a和b,還是x和y?
我相信你這時候你恍然大悟了,我們一直在交換a和b,並沒有操作x和y

那麼我們怎麼操作?指針!
因為x和y在整個程序中的地址一定是不變的,那麼我們通過上一節的指針運算可以得到,我們能夠經過指針操作變量的值。
那麼,我們改進一下這個函數

void swap(int* a,int* b){
      int t;
      t = *a;
      *a = *b;
      *b = t;
}

我們再來試試,然後你就會得到報錯信息。

我想,你是這麼用的:swap(x,y)
問題就在這裏,我們看看swap需要怎樣的兩個變量?int*int*類型。
怎麼辦?我告訴你一個小秘密:
任何一個變量加上&,此時就相當於在原本的類型加上了*
什麼意思?也就是說:

int a;
&a ---> int*;
double d;
&d ---> double*;
int* p;
&p ---> int**;//這是個二級指針,也就是說指向指針的指針

那麼,我們要這麼做:swap(&a,&b),把傳入的參數int換為int*

再次嘗試,得到輸出:

x=5,y=10
x=10,y=5

累死了,總算是搞好了

Part 5:char*表示字符串

char*這個神奇的類型可以表示個字符串,舉個例子:


#include <stdio.h>

int main()
{
    char* str;
    str = "YOU AK IOI!";
    printf("%s",str);
}

請注意:輸入和輸出字符串的時候,都不能帶上*&

你可以用string.h中的函數來進行操作

Part 6:野指針

有些同學他會這麼寫:

int* p;
printf("%p",p);

哦千萬不要這麼做!
當你沒有讓p指向某個地方的時候,你還把他用了!這個時候就會產生野指針。
野指針的危害是什麼?
第一種是指向不可訪問(操作系統不允許訪問的敏感地址,譬如內核空間)的地址,結果是觸發段錯誤,這種算是最好的情況了;

第二種是指向一個可用的、而且沒什麼特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程序運行不會出錯,也不會對當前程序造成損害,這種情況下會掩蓋你的程序錯誤,讓你以為程序沒問題,其實是有問題的;

第三種情況就是指向了一個可用的空間,而且這個空間其實在程序中正在被使用(譬如說是程序的一個變量x),那麼野指針的解引用就會剛好修改這個變量x的值,導致這個變量莫名其妙的被改變,程序出現離奇的錯誤。一般最終都會導致程序崩潰,或者數據被損害。這種危害是最大的。

不論如何,我們都不希望看到這些發生。
於是,養成好習慣:變量先賦值。

指針你可以這麼做:int *p =NULL;讓指針指向空

不論如何,他總算有個值了。

Part 7:總結

本文乾貨全部在這裏了:

  1. 指針是個變量,他的類型是數據類型+*,他的值是一個地址,他自身也有地址
  2. 指針有兩個專屬運算符:&*
  3. 指針可以操作變量,不能操作常量
  4. 指針可以表示字符串
  5. 請注意野指針的問題

本文沒有講到的:

  1. char[],char,const char的區別與聯繫
  2. const修飾指針會怎麼樣?
  3. void*指針的運用
  4. 多級指針的運用
  5. NULL到底是什麼
  6. malloc函數的運用

感謝觀看!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

軟件設計模式學習(二十四)狀態模式

狀態模式用於解決系統中複雜對象的狀態轉換以及不同狀態下行為的封裝問題

模式動機

很多情況下,一個對象的行為取決於一個或多個動態變化的屬性,這樣的屬性叫做狀態。一個對象可以擁有多個狀態,這些狀態可以相互轉換,當對象狀態不同時,其行為也有所差異。

假設一個人就是對象,人根據心情不同會有很多狀態,比如開心和傷心,這兩種狀態可以相互轉換。開心的人可能會突然接到女朋友的分手電話,然後哭得稀里嘩啦(醒醒!你哪來的女朋友?),過了一段時間后,又可能因為中了一百萬彩票而歡呼雀躍。而且不同狀態下人的行為也不同,有些人傷心時會通過運動、旅行、聽音樂來緩解心情,而開心時則可能會唱歌、跳舞、請客吃飯等等。

再來考慮軟件系統中的情況,如某酒店訂房系統,可以將房間設計為一個類,房間對象有已預訂、空閑、已入住等情況,這些狀態之間可以相互轉換,並且不同狀態的對象可能具有不同的行為,如已預訂或已入住的房間不能再接收其他顧客的預訂,而空閑的房間可以接受預訂。

在過去我們遇到這種情況,可以使用複雜的條件判斷來進行狀態判斷和轉換操作,這會導致代碼的可維護性和靈活性下降,當出現新的狀態時必須修改源代碼,違反了開閉原則。在狀態模式中,可以將對象狀態從包含該狀態的類中分離出來,做成一個個單獨的狀態類,如人的兩種情緒可以設計成兩個狀態類:

將開心與傷心兩種情緒從“人”中分離出來,從而避免在“人”中進行狀態轉換和判斷,將擁有狀態的對象和狀態對應的行為分離,這就是狀態模式的動機。

模式定義

允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類。其別名為狀態對象(Objects for States),狀態模式是一種對象行為型模式。

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

模式結構與分析

我們把擁有狀態的對象稱為環境類,也叫上下文類。再引入一個抽象狀態類來專門表示對象的狀態,對象的每一種具體狀態類都繼承該抽象類,不同具體狀態類實現不同狀態的行為,包括各種狀態之間的轉換。在環境類中維護一個抽象狀態類 State 的實例,用來定義當前狀態。

得到狀態模式結構類圖如下:

環境類中的 request() 方法處理業務邏輯,根據狀態去調用對應的 handle() 方法,如果需要切換狀態,還提供了 setState() 用於設置當前房間狀態。如果我們希望執行操作后狀態自動發生改變,那麼我們還需要在 State 中定義一個 Context 對象,實現一個雙向依賴關係。

考慮前面提到的訂房系統,如果不使用狀態模式,可能就會存在如下代碼:

if (state == "空閑") {
	if (預訂房間) {
        預訂操作;
        state = "已預訂";
	} else if (住進房間) {
    	入住操作;
        state = "已入住";
    }
} else if(state == "已預訂") {
	if (住進房間) {
        入住操作;
        state = "已入住";
	} else if (取消預訂) {
    	取消操作;
        state = "空閑";
    }
}

上述代碼需要做頻繁且複雜的判斷操作,可維護性很差。因此考慮使用狀態模式將房間類的狀態分離出來,將與每種狀態有關的操作封裝在獨立的狀態類中。

我們來寫一個完整的示例

環境類(Room)

public class Room {
	
    // 維護一個狀態對象
    private State state;

    public Room() {
        // 默認為空閑狀態
        this.state = new IdleState(this);
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public void reserve() {
        state.reserve();
    }

    public void checkIn() {
        state.checkIn();
    }

    public void cancelReserve() {
        state.cancelReserve();
    }

    public void checkOut() {
        state.checkOut();
    }
}

抽象狀態類(State)

public abstract class State {
	
    // 用於狀態轉換
    protected Room room;

    public State(Room room) {
        this.room = room;
    }

    public abstract void reserve();

    public abstract void checkIn();

    public abstract void cancelReserve();

    public abstract void checkOut();
}

具體狀態類(IdleState)

public class IdleState extends State {

    public IdleState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("房間預訂成功");
        // 	切換狀態
        room.setState(new ReservedState(room));
    }

    @Override
    public void checkIn() {
        System.out.println("房間入住成功");
        room.setState(new InhabitedState(room));
    }

    @Override
    public void cancelReserve() {
        System.out.println("無法取消預訂,房間處於空閑狀態");
    }

    @Override
    public void checkOut() {
        System.out.println("無法退房,房間處於空閑狀態");
    }

}

具體狀態類(ReservedState)

public class ReservedState extends State {

    public ReservedState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("無法預訂,房間處於已預訂狀態");
    }

    @Override
    public void checkIn() {
        System.out.println("房間入住成功");
        room.setState(new InhabitedState(room));
    }

    @Override
    public void cancelReserve() {
        System.out.println("取消預訂成功");
        room.setState(new IdleState(room));
    }

    @Override
    public void checkOut() {
        System.out.println("無法退房,房間處於已預訂狀態");
    }
}

具體狀態類(InhabitedState)

public class InhabitedState extends State {

    public InhabitedState(Room room) {
        super(room);
    }

    @Override
    public void reserve() {
        System.out.println("無法預訂,房間處於入住狀態");
    }

    @Override
    public void checkIn() {
        System.out.println("無法入住,房間處於入住狀態");
    }

    @Override
    public void cancelReserve() {
        System.out.println("無法取消預訂,房間處於入住狀態");
    }

    @Override
    public void checkOut() {
        System.out.println("退房成功");
        room.setState(new IdleState(room));
    }
}

客戶端測試類(Client)

public class Client {

    public static void main(String[] args) {

        Room room = new Room();
        room.cancelReserve();
        room.checkOut();
        room.reserve();
        System.out.println("--------------------------");
        room.reserve();
        room.checkOut();
        room.checkIn();
        System.out.println("--------------------------");
        room.reserve();
        room.checkIn();
        room.cancelReserve();
        room.checkOut();
    }
}

運行結果

模式優缺點

狀態模式的優點:

  • 封裝了轉換規則,將不同狀態之間的轉換狀態封裝在狀態類中,避免了冗長的條件判斷,提高了代碼的可維護性
  • 將所有與某個規則有關的行為放到一個類,可以很方便地增加新的狀態
  • 可以讓多個環境對象共享一個狀態對象,從而減少系統中對象的個數

狀態模式的缺點:

  • 增加了系統類和對象的個數
  • 結構較為複雜,使用不當將導致代碼混亂
  • 對於可以切換狀態的狀態模式,增加新的狀態類需要修改負責狀態轉換的代碼

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

09_EM算法

  今天是2020年3月5日星期四。預計開學時間不會早於四月初,真是好消息,可以有大把的時間整理知識點(實際上發文章的時間都6月6號了,希望9月份能開學啊,不耽誤找工作~)。每次導師找,整個人會變的特別煩躁,煩躁加不安,其它事情一點都做不下去,焦慮。改小論文這幾天耽誤了一些時間,查了些EM算法的例子,怎樣理解這個算法呢?通過這周的學習,覺得數學公式有點唬人,但卻是理解該算法最好的形式。

  剛開始對這個算法一無所知,通過知乎、CSDN看資料,看白板視頻,看講解例子。越看例子越覺得負擔重,因為要先把例子理解了,再去理解這個知識點。例子不能徹底理解,知識點也走不下去,倒不如一遍一遍的看數學公式。看完了公式,再去看例子,朦朦朧朧的就懂了。之後再去看白板視頻,絕對是不一樣的體驗。

  先看別人的視頻,然後自己去推導公式,你會覺得困難摸不到頭腦;先自己去推導公式,再去看別人視頻,你會覺得心曠神怡一目瞭然。第一種做法,往往看視頻的時候就是懵懵噠,抓不住別人講述的重點;第二種做法,類似於先學會了九陽神功,再去和別人切磋武藝。初心是將《統計學習方法》這本書做詳細的心得筆記,現在有點鬆動,希望能堅持下去。

 GitHub:https://github.com/wangzycloud/statistical-learning-method

 EM算法

引入

  EM算法應該作為一種通用的求解方法,用於含有隱變量的概率模型參數的極大似然估計。拆開來看,這句話是應用在概率模型上的;用來估計概率模型的參數;類似於極大似然估計;求解的是含有隱變量的概率模型。那麼問題來了,什麼是該有隱變量的概率模型?概率模型是什麼樣子?極大似然估計?該方法是怎麼進行計算的呢?

  通常來講,EM算法是一種迭代算法,每次迭代由兩步組成:E步,求期望;M步:求極大,所以該算法被稱為期望極大算法。說該算法可以作為一種通用的求解方法,原因在於:該算法不是NBM、LR、SVM這類解決相應場景的模型,而是可以用於求解含有隱變量概率模型的參數估計。

  提到模型,腦子里第一印象有判別模型、生成模型。這裏的概率模型自然和判別模型、生成模型不在同一個層次。在我的理解里,概率模型是類似於樸素貝恭弘=叶 恭弘斯算法這種,用概率來表示最後的分類標準;而不是感知機、SVM這種利用確信度來表達分類結果的模型。再考慮一下樸素貝恭弘=叶 恭弘斯算法,特徵向量里的隨機變量X,以及表示類別的隨機變量Y,都是可以被觀測到變量。在所有隨機變量都可以觀測到的情況下,我們可以利用極大似然估計來求解模型的參數。對於含有隱變量的概率模型,要如何求解呢?含有隱變量意味着不能觀測到數據的全部狀況,也就沒有辦法直接利用極大似然估計來求解。

  現在看到的EM算法,就是一種求解含有隱變量的概率模型參數的極大似然估計方法。

EM算法

  書本上三硬幣模型,挺好的~代碼已整理到github中,實際上就是把書本公式用代碼實現出來…難度不大。

   文中提到,該問題沒有解析解,只有通過迭代的方法進行求解。仔細觀察一下公式(9.4),log(x)作用在公式(9.3)上,很明顯log連乘可以變成連加,但連加式子中的每個項仍然是連加式。好像是因為這個原因,就無法得到解析解了。個人對數學不感冒,只能硬性的記住“不容易求解析解”這點,至於原因,實在是搞不懂啊。雖然無法得到解析解,但我們可以通過EM算法求解,大致步驟如下:

   一般的,用Y表示觀測隨機變量的數據,Z表示隱隨機變量的數據,Y和Z連在一起稱為完全數據,觀測數據Y又稱為不完全數據。假設給定觀測數據Y,其概率分佈是P(Y|θ),其中θ是需要估計的模型參數,那麼不完全數據Y的似然函數是P(Y|θ),對數似然函數L(θ)=logP(Y|θ),假設Y和Z的聯合概率分佈是P(Y,Z|θ),那麼完全數據的對數似然函數是logP(Y,Z|θ)。

  EM算法通過迭代求解L(θ)=logP(Y|θ)的極大似然估計,每次迭代由兩個步驟:E步,M步組成。

  文中對Q函數做了具體解釋:

   關於EM算法的幾點說明,應該挺好理解的吧。步驟(1),迭代求解的方式需要一步步接近極值,是在某個解的基礎上,進一步求解。在最開始的時候,初值是任意選擇的,並且正是因為初值任意選擇,容易陷入局部極值,也就是對初值的選擇非常敏感(對比一下梯度下降的過程)。步驟(2),我們要清楚,求解的對象是變元參數θ。步驟(3),極大化的過程,詳見下圖~(θ,L(θ))圖像。步驟(4),迭代停止條件。

  EM算法的導出、收斂性,以及推廣詳見下圖吧~搞了四五天,弄了個流程…

GMM高斯混合模型

   書中公式一大堆,不太友好,手寫代碼的過程,就是把書本公式復現了一遍。難度不大,我認為需要先了解GMM模型是啥,再通過例子,熟悉一下計算過程,就可以掌握了。

  還是從生成數據的角度看,由GMM模型生成一個數據,是要根據一個普通的多項式分佈αk,來選擇第k個高斯分佈,分兩步生成數據。但是,這裏獲得的數據,並不知道來自第幾個αk,這就是隱變量了。

   對於高斯混合模型的參數估計,可以通過EM算法求解。

  1.明確隱變量,寫出完全數據的對數似然函數。

  2.EM算法的E步:確定Q函數。

  3.確定EM算法的M步。

  具體公式(9.26)-公式(9.32)就不一一摘錄了,github已復現。算法描述如下:

  本節整理的內容有些水…

代碼效果

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

對於單例模式面試官會怎樣提問呢?你又該如何回答呢?

前言

在面試的時候面試官會怎麼在單例模式中提問呢?你又該如何回答呢?可能你在面試的時候你會碰到這些問題:

  • 為什麼說餓漢式單例天生就是線程安全的?

  • 傳統的懶漢式單例為什麼是非線程安全的?

  • 怎麼修改傳統的懶漢式單例,使其線程變得安全?

  • 線程安全的單例的實現還有哪些,怎麼實現?

  • 雙重檢查模式、Volatile關鍵字 在單例模式中的應用

  • ThreadLocal 在單例模式中的應用

  • 枚舉式單例

那我們該怎麼回答呢?那答案來了,看完接下來的內容就可以跟面試官嘮嘮單例模式了

 

單例模式簡介

單例模式是一種常用的軟件設計模式,其屬於創建型模式,其含義即是一個類只有一個實例,併為整個系統提供一個全局訪問點 (向整個系統提供這個實)。

結構:

                      

單例模式三要素:

  • 私有的構造方法;

  • 私有靜態實例引用;

  • 返回靜態實例的靜態公有方法。

單例模式的優點

  • 在內存中只有一個對象,節省內存空間;

  • 避免頻繁的創建銷毀對象,可以提高性能;

  • 避免對共享資源的多重佔用,簡化訪問;

  • 為整個系統提供一個全局訪問點。

單例模式的注意事項

  在使用單例模式時,我們必須使用單例類提供的公有工廠方法得到單例對象,而不應該使用反射來創建,使用反射將會破壞單例模式 ,將會實例化一個新對象。

 

單線程實現方式

在單線程環境下,單例模式根據實例化對象時機的不同分為,

  • 餓漢式單例(立即加載)餓漢式單例在單例類被加載時候,就實例化一個對象並將引用所指向的這個實例;

  • 懶漢式單例(延遲加載),只有在需要使用的時候才會實例化一個對象將引用所指向的這個實例。

 

從速度和反應時間角度來講,餓漢式(又稱立即加載)要好一些;從資源利用效率上說,懶漢式(又稱延遲加載)要好一些。

餓漢式單例

// 餓漢式單例
public class HungrySingleton{
​
    // 私有靜態實例引用,創建私有靜態實例,並將引用所指向的實例
    private static HungrySingleton singleton = new HungrySingleton();
    // 私有的構造方法
    private HungrySingleton(){}
    //返回靜態實例的靜態公有方法,靜態工廠方法
    public static HungrySingleton getSingleton(){
        return singleton;
    }
}

餓漢式單例,在類被加載時,就會實例化一個對象並將引用所指向的這個實例;更重要的是,由於這個類在整個生命周期中只會被加載一次,只會被創建一次,因此惡漢式單例線程安全的。

那餓漢式單例為什麼是天生就線程安全呢?

因為類加載的方式是按需加載,且只加載一次。由於一個類在整個生命周期中只會被加載一次,在線程訪問單例對象之前就已經創建好了,且僅此一個實例。即線程每次都只能也必定只可以拿到這個唯一的對象。

懶漢式單例

// 懶漢式單例
public class LazySingleton {
    // 私有靜態實例引用
    private static LazySingleton singleton;
    // 私有的構造方法
    private LazySingleton(){}
    // 返回靜態實例的靜態公有方法,靜態工廠方法
    public static LazySingleton getSingleton(){
        //當需要創建類的時候創建單例類,並將引用所指向的實例
        if (singleton == null) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

懶漢式單例是延遲加載,只有在需要使用的時候才會實例化一個對象,並將引用所指向的這個對象。

由於是需要時創建,在多線程環境是不安全的,可能會併發創建實例,出現多實例的情況,單例模式的初衷是相背離的。那我們需要怎麼避免呢?可以看接下來的多線程中單例模式的實現形式。

那為什麼傳統的懶漢式單例為什麼是非線程安全的?

非線程安全主要原因是,會有多個線程同時進入創建實例(if (singleton == null) {}代碼塊)的情況發生。當這種這種情形發生后,該單例類就會創建出多個實例,違背單例模式的初衷。因此,傳統的懶漢式單例是非線程安全的。

 

多線程實現方式

  在單線程環境下,無論是餓漢式單例還是懶漢式單例,它們都能夠正常工作。但是,在多線程環境下就有可能發生變異:

  • 餓漢式單例天生就是線程安全的,可以直接用於多線程而不會出現問題

  • 懶漢式單例本身是非線程安全的,因此就會出現多個實例的情況,與單例模式的初衷是相背離的。

 

那我們應該怎麼在懶漢的基礎上改造呢?

  • synchronized方法

  • synchronized塊

  • 使用內部類實現延遲加載

synchronized方法

// 線程安全的懶漢式單例
public class SynchronizedSingleton {
    private static SynchronizedSingleton synchronizedSingleton;
    private SynchronizedSingleton(){}
    // 使用 synchronized 修飾,臨界資源的同步互斥訪問
    public static synchronized SynchronizedSingleton getSingleton(){
        if (synchronizedSingleton == null) {
            synchronizedSingleton = new SynchronizedSingleton();
        }
        return synchronizedSingleton;
    }
}

  使用 synchronized 修飾 getSingleton()方法,將getSingleton()方法進行加鎖,實現對臨界資源的同步互斥訪問,以此來保證單例。

雖然可現實線程安全,但由於同步的作用域偏大、鎖的粒度有點粗,會導致運行效率會很低。

synchronized塊

// 線程安全的懶漢式單例
public class BlockSingleton {
    private static BlockSingleton singleton;
    private BlockSingleton(){}
    public static BlockSingleton getSingleton2(){
        synchronized(BlockSingleton.class){  // 使用 synchronized 塊,臨界資源的同步互斥訪問
            if (singleton == null) { 
                singleton = new BlockSingleton();
            }
        }
        return singleton;
    }
}

 其實synchronized塊跟synchronized方法類似,效率都偏低。

使用內部類實現延遲加載

// 線程安全的懶漢式單例
public class InsideSingleton {
    // 私有內部類,按需加載,用時加載,也就是延遲加載
    private static class Holder {
        private static InsideSingleton insideSingleton = new InsideSingleton();
    }
    private InsideSingleton() {
    }
    public static InsideSingleton getSingleton() {
        return Holder.insideSingleton;
    }
}
  • 如上述代碼所示,我們可以使用內部類實現線程安全的懶漢式單例,這種方式也是一種效率比較高的做法。其跟餓漢式單例原理是相同的, 但可能還存在反射攻擊或者反序列化攻擊 。

 

雙重檢查(Double-Check idiom)現實

雙重檢查(Double-Check idiom)-volatile

使用雙重檢測同步延遲加載去創建單例,不但保證了單例,而且提高了程序運行效率。

// 線程安全的懶漢式單例
public class DoubleCheckSingleton {
    //使用volatile關鍵字防止重排序,因為 new Instance()是一個非原子操作,可能創建一個不完整的實例
    private static volatile DoubleCheckSingleton singleton;
    private DoubleCheckSingleton() {
    }
​
    public static DoubleCheckSingleton getSingleton() {
        // Double-Check idiom
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {       
                // 只需在第一次創建實例時才同步
                if (singleton == null) {      
                    singleton = new DoubleCheckSingleton();      
                }
            }
        }
        return singleton;
    }
​
}

為了在保證單例的前提下提高運行效率,我們需要對singleton實例進行第二次檢查,為的式避開過多的同步(因為同步只需在第一次創建實例時才同步,一旦創建成功,以後獲取實例時就不需要同步獲取鎖了)。

但需要注意的必須使用volatile關鍵字修飾單例引用,為什麼呢?

 如果沒有使用volatile關鍵字是可能會導致指令重排序情況出現,在Singleton 構造函數體執行之前,變量 singleton可能提前成為非 null 的,即賦值語句在對象實例化之前調用,此時別的線程將得到的是一個不完整(未初始化)的對象,會導致系統崩潰。

此可能為程序執行步驟:

  1. 線程 1 進入 getSingleton() 方法,由於 singleton 為 null,線程 1 進入 synchronized 塊 ;

  2. 同樣由於 singleton為 null,線程 1 直接前進到 singleton = new DoubleCheckSingleton()處,在new對象的時候出現重排序,導致在構造函數執行之前,使實例成為非 null,並且該實例並未初始化的(原因在NOTE);

  3. 此時,線程 2 檢查實例是否為 null。由於實例不為 null,線程 2 得到一個不完整(未初始化)的 Singleton 對象

  4. 線程 1 通過運行 Singleton對象的構造函數來完成對該對象的初始化。

  這種安全隱患正是由於指令重排序的問題所導致的。而volatile 關鍵字正好可以完美解決了這個問題。使用volatile關鍵字修飾單例引用就可以避免上述災難。

NOTE

new 操作會進行三步走,預想中的執行步驟:

memory = allocate();        //1:分配對象的內存空間
ctorInstance(memory);       //2:初始化對象
singleton = memory;        //3:使singleton3指向剛分配的內存地址

**但實際上,這個過程可能發生無序寫入(指令重排序),可能會導致所下執行步驟:

memory = allocate();        //1:分配對象的內存空間
singleton3 = memory;        //3:使singleton3指向剛分配的內存地址
ctorInstance(memory);       //2:初始化對象

雙重檢查(Double-Check idiom)-ThreadLocal

  藉助於 ThreadLocal,我們可以實現雙重檢查模式的變體。我們將臨界資源線程局部化,具體到本例就是將雙重檢測的第一層檢測條件 if (instance == null) 轉換為 線程局部範圍內的操作 。

// 線程安全的懶漢式單例
public class ThreadLocalSingleton 
    // ThreadLocal 線程局部變量
    private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>();
    private static ThreadLocalSingleton singleton = null;
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getSingleton(){
        if (threadLocal.get() == null) {        // 第一次檢查:該線程是否第一次訪問
            createSingleton();
        }
        return singleton;
    }
​
    public static void createSingleton(){
        synchronized (ThreadLocalSingleton.class) {
            if (singleton == null) {          // 第二次檢查:該單例是否被創建
                singleton = new ThreadLocalSingleton();   // 只執行一次
            }
        }
        threadLocal.set(singleton);      // 將單例放入當前線程的局部變量中 
    }
}

藉助於 ThreadLocal,我們也可以實現線程安全的懶漢式單例。但與直接雙重檢查模式使用,使用ThreadLocal的實現在效率上還不如雙重檢查鎖定。

 

枚舉實現方式

它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,

直接通過Singleton.INSTANCE.whateverMethod()的方式調用即可。方便、簡潔又安全。

public enum EnumSingleton {
    instance;
    public void whateverMethod(){
        //dosomething
    }
}

 

 

測試單例線程安全性

 使用多個線程,並使用hashCode值計算每個實例的值,值相同為同一實例,否則為不同實例。

public class Test {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new TestThread();
​
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
​
        }
    }
}
class TestThread extends Thread {
    @Override
    public void run() {
        // 對於不同單例模式的實現,只需更改相應的單例類名及其公有靜態工廠方法名即可
        int hash = Singleton5.getSingleton5().hashCode();  
        System.out.println(hash);
    }
}

 

 

小結

單例模式是 Java 中最簡單,也是最基礎,最常用的設計模式之一。在運行期間,保證某個類只創建一個實例,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點 ,介紹單例模式的各種寫法:

  • 餓漢式單例(線程安全)

  • 懶漢式單例

    • 傳統懶漢式單例(線程安全);

    • 使用synchronized方法實(線程安全);

    • 使用synchronized塊實現懶漢式單例(線程安全);

    • 使用靜態內部類實現懶漢式單例(線程安全)。

  • 使用雙重檢查模式

    • 使用volatile關鍵字(線程安全);

    • 使用ThreadLocal實現懶漢式單例(線程安全)。

  • 枚舉式單例

 

各位看官還可以嗎?喜歡的話,動動手指點個,點個關注唄!!謝謝支持! 歡迎關注公眾號【Ccww技術博客】,原創技術文章第一時間推出

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

單獨囚禁在水池2年 日本311倖存海豚抑鬱離世

3{icon} {views}

摘錄自2020年4月15日鏡週刊報導

國際動保組織「海豚計畫」(Dolphin project)報導指出,日本寬吻海豚「Honey」於2005年在日本太地町被捕獲,自此生活在千葉線犬吠埼海洋公園僅80平方公尺大的水池中,與另一隻海豚、46隻企鵝及數百種魚類一起生活。

311大地震後面臨觀光衰退和建築物老舊,2008年11月犬吠埼海洋公園被爆出營運困難負債,正尋找買家,去年初終於轉賣,但如今整個園區卻呈現廢棄狀態,也沒有遊客造訪,僅安排員工前來餵食。另一隻海豚於2017年死亡,「Honey」自此單獨每天在這廢棄的水池中游來游去,對習性群居的海豚而言,這無疑是極大的折磨。

動保組織「海豚計畫」曾試圖聯繫買下海豚「Honey」,盼為牠找到合適的居所安享晚年,但為時已晚,3月初「Honey」狀況已經不太好,3月29日牠因阻塞性腸炎,死在待了泰半生的水池。

國際新聞
日本
核災
展示動物
動物福利

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

殼牌石油拚減碳 誓言最遲2050年達零排放

4{icon} {views}

摘錄自2020年4月16日聯合報報導

國際石油天然氣巨擘荷蘭皇家殼牌集團(Royal Dutch Shell)今(16日)誓言,在2050年前要達成「碳中和」(Carbon Neutral)目標,和競爭對手英國石油公司(BP)的承諾一樣。

法新社報導,殼牌執行長范柏登(Ben vanBeurden)在聲明中表示,社會對於氣候變遷的期許瞬息萬變,殼牌需要再進一步自我要求,計畫最晚在2050年成為零排放的能源企業。殼牌將在2050年前把自家能源產品的「淨碳足跡」減少約65%;在2030年減少30%。

溫室氣體
能源議題
全球變遷
氣候變遷
能源轉型
國際新聞
殼牌
減碳宣言
零排放
石油

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

日本大鯢數量急減 雜交問題嚴重 水族館推大鯢布偶提升國民關注

1{icon} {views}

文:宋瑞文

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

4歲男童閒晃突遇野豬群襲擊 身體遭啃食送醫不治

4{icon} {views}

摘錄自2020年4月24日聯合報報導

印度近期發生一起驚悚案件,一位4歲男童21日在家中附近閒晃玩耍時,突遇野豬群襲擊,被拖進一處垃圾場中攻擊啃咬,事後警方發現男童時,他全身多處已被豬群啃食,直到男童父母到場後才識別出他的身份,而男童送醫後則宣告不治。

根據英國「太陽報」報導,男童哈沙(V. Harshavardhan)居住在印度海得拉巴(Hyderabad)的賽義達巴德(Saidabad),當地居民曾多次向地方政府投訴,反應受到野豬群干擾,當地人表示沒有辦法阻止這些野豬瘋狂奔跑,雖然野豬已經被趕走,但牠們還是會找到路回來。

生活環境
國際新聞
印度
野豬

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!