20191004のJavaに関する記事は7件です。

FileInputStreamとBufferedInputStreamまとめ

概要

以前どこかでFileInputStreamをBufferedStreamにすると処理が早くなるという記事を見つけました。
そこで今回はFileInputStreamとBufferedInputStreamについて記事を描いてみようと思います。

2019/10/06 追記
コメントにて、なぜInputStreamReaderを使用する場合はFileInputStreamでも処理が高速化されるのかを解説していただきました。
ぜひご参照ください。

二つの違いは?

両者の違いは以下を読むとわかりやすかったです。

これは、ディスクを使用して1バイトを読み取るOSへのネイティブ呼び出しです。これは重い操作です。

BufferedInputStreamを使用すると、このメソッドは、8192バイトのバイトを読み込み、必要になるまでバッファリングする、オーバーロードされたread()メソッドに委譲します。依然として1バイトしか返されません(ただし、他のバイトは予約されています)。このようにして、BufferedInputStreamはOSにネイティブ呼び出しを少なくしてファイルから読み込みます。

BufferedInputStreamを使用してFileInputStreamを使用するよりも速くバイト単位でファイルを読み取るのはなぜですか?

つまり、FileInputStreamは一度に1バイトしか読み取りを行わないため多数のディスクアクセスが発生するのに対し、BufferedInputStreamは一度に大量のバイト数を読みとるためより少ないディスクアクセスでデータの読み取りが可能であるということです。

実際に実験して見た結果が以下です。
まず、読みとるファイルを以下のようにして作成します。

以下のようにして作成したファイルを読み込む
final FileOutputStream fos = new FileOutputStream(filePath);
final BufferedOutputStream bos = new BufferedOutputStream(fos);
final OutputStreamWriter osw = new OutputStreamWriter(bos, Charset.defaultCharset());
final PrintWriter pr = new PrintWriter(osw);
for (int i = 0; i < 500000; i++) {
    pr.println("あ");
}
pr.close();

処理1としてFileInputStreamで上記のファイルを読み取った後に、読み取ったものをStringBuilderにappendする処理をやりました。

処理1_FileInputStream
//FileInputStream
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);

//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
    line = inputStream.read();
    if (line == -1) {
        break;
    }
    sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();

処理2ではFileInputStreamをBufferedInputStreamでラップして使用しました。

処理2_BufferedInputStream
//BufferedInputStream
StringBuilder sb = new StringBuilder();
final FileInputStream fis = new FileInputStream(filePath);
BufferedInputStream inputStream = new BufferedInputStream(fis);

//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
    line = inputStream.read();
    if (line == -1) {
        break;
    }
    sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
fis.close();

結果は以下の通りです(単位はms)。

FileInputStream

1回目: 3840
2回目: 3820
3回目: 3772

BufferedInputStream

1回目: 109
2回目: 111
3回目: 117

一目瞭然ですね。明らかにBufferedInputStreamの方が高速であることがわかります。
ちなみにFor文の回数を50回にしたところ、FileInputStreamでは2761196ns、BufferedInputStreamでは2195839nsとあまり変わらない数値となりました。

InputStreamReaderを使えば違いはない?

読み込んだファイルのバイト列を、文字に変換するにはInputStreamReaderクラスを使用しますが、実はInputStreamReaderを使用することによってFileInputStreamとBufferedInputStreamの違いはほとんど無くなってしまうことがわかりました。

処理3_FileInputStream+InputStreamReader
//FileInputStream + InputStreamReader
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
final InputStreamReader reader = new InputStreamReader(inputStream);

//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
    line = reader.read();
    if (line == -1) {
        break;
    }
    sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
reader.close();
処理4_BufferdInputStream+InputStreamReader
//BufferdInputStream + InputStreamReader
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
final BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
final InputStreamReader reader = new InputStreamReader(bufferedInputStream);

//処理
long startTime = System.currentTimeMillis();
int line;
while (true) {
    line = reader.read();
    if (line == -1) {
        break;
    }
    sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();
reader.close();

結果は以下の通りです(単位はms)。

FileInputStream + InputStreamReader

1回目: 114
2回目: 131
3回目: 154

BufferedInputStream + InputStreamReader

1回目: 163
2回目: 167
3回目: 150

BufferedInputStreamのインスタンスの生成時間があるからでしょうか、結果だけ見るとInputStreamReaderを使用する際はFileInputStreamをラップしないでファイル読み取りをやる方が早いように見えますね。

読み取ったファイルを文字列としてStringBuilderなどに出力する場合は、InputStreamReaderを使うことになると思いますので、FileInputStreamでもBufferedInputStreamでもあまり変わらないということになりますね。
InputStreamReaderの説明については以下を参照ください。

InputStreamReaderとは : JavaA2Z

じゃあ常にBufferedInputStreamを使えばよいのか?

明らかにBufferedInputStreamの方が処理スピードが早いので使った方がよいと思われるかもしれませんが、実は使ってもあまり効果がないという場合が存在します。

それはreadメソッドの引数にバッファサイズを設定したときです。
InputStreamではread()の引数としてバッファサイズを設定することができ、一度に読みとるバイト数を制御することが可能です。
何も設定していない場合は一度に1バイトずつの読み取りとなってしまいます(BufferedInputStreamは例外ですが)。

ですので、例えば上記処理1や処理2のコードを以下のように書き換えるならば、速度的な面で言えば、あまり変わらなくなってしまいます。

処理1のread()にバッファサイズを指定してみる
//処理1 FileInputStream
StringBuilder sb = new StringBuilder();
final FileInputStream inputStream = new FileInputStream(filePath);
// BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
long startTime = System.currentTimeMillis();
int line;
while (true) {
    line = inputStream.read(new byte[8192]);
    if (line == -1) {
        break;
    }
    sb.append(line);
}
long endTime = System.currentTimeMillis();
System.out.println("処理時間 : " + (endTime - startTime));
inputStream.close();

read(new byte[8192])として、FileInputStreamとBufferedInputStreamの両方で処理を行ったところ、両者とも実行時間は3msとなり、結果的にあまり変わらない速さとなりました。

これについては以下のサイトの記載が参考になったので引用します。

大量の小さな読み取り(一度に1バイトまたは数バイト)を実行する可能性がある場合、またはバッファーAPIによって提供される高水準の機能を使用したい場合は、意味があります。例えばBufferedReader.readLine()メソッドです。

ただし、read(byte [])メソッドまたはread(byte []、int、int)メソッドを使用して大きなブロックの読み込みのみを実行する場合は、BufferedInputStreamでInputStreamをラップしても効果がありません。

java – InputStreamを常にBufferedInputStreamとしてラップするべきですか

後は、上記しましたが、入力するファイルサイズが極端に小さい場合もどちらを使っても速度的にはあまり変わらないのでラップする効果は薄いです。

まとめ

この記事を書く前は「FileInputStreamよりBufferedInputStreamの方が絶対速いだろう」と思っていたのですが、InputStreamReader(BufferedInputSteramReader)を使うと、両者はあまり違わないことがわかりました。

また、BufferedInputStreamを使用してもあまり効果がない場合が存在することも勉強になりました。

どんなクラスでもそうですが、時と場合に合わせて適切なクラスを選択することが大切になりそうですね。

その他参考:
BufferedInputStreamの威力を実験してみる
FIO10-J. read() を使って配列にデータを読み込むときには配列への読み込みが意図した通りに行われたことを確認する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HerokuでCorretto 11を使う

ビルドパックの設定

$ heroku buildpacks:set jdk/corretto -a [App名]
$ heroku buildpacks:add heroku/java -a [App名]

環境変数の追加

$ heroku config:set CORRETTO_URL=https://d3pxv6yz143wms.cloudfront.net/11.0.4.11.1/amazon-corretto-11.0.4.11.1-linux-x64.tar.gz \
  CORRETTO_CHECKSUM=4bbcd5e6d721fef56e46b3bfa8631c1c \
  -a [App name]

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

spring-securityでsetDetailsの中のリストを画面表示する

spring-securityでUsernamePasswordAuthenticationToken#setDetailsでセットした任意オブジェクトのリストのプロパティをThymeleafで表示する。

ソースコード

pom.xml
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class App {
  public static void main(String[] args) {
    new SpringApplicationBuilder(App.class).web(WebApplicationType.SERVLET).run(args);
  }
}

とりあえず、認証はなんもせずにsetDetailsで値を詰めるだけをする。

import java.util.Arrays;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

@Component
public class MyAuthenticationProvider implements AuthenticationProvider {

  public Authentication authenticate(Authentication arg0) throws AuthenticationException {
    UsernamePasswordAuthenticationToken t = new UsernamePasswordAuthenticationToken("kagami", "hoge");
    MyDetail d = new MyDetail();
    d.hogeValue = "hoge";
    d.hogeList = Arrays.asList("aaa", "bbb", "ccc");
    t.setDetails(d);
    return t;
  }

  public boolean supports(Class<?> arg0) {
    return true;
  }
}

import java.util.List;

import lombok.Data;

@Data
public class MyDetail {
  String hogeValue;
  List<String> hogeList;
}

デフォルトのログイン画面で適当なid-password入力すると、以下のhtmlを表示する。

/springsecsample/src/main/resouces/templates/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
    <div th:text="${#authentication.details.hogeValue}"></div>
    <table>
        <tr th:each="hoge : ${#authentication.details.hogeList}">
            <td th:text="${hoge}"></td>
        </tr>
    </table>
</body>
</html>

https://github.com/thymeleaf/thymeleaf-extras-springsecurity にあるように、#authenticationでspring-securityの認証オブジェクト(ここではMyAuthenticationProviderが返すUsernamePasswordAuthenticationTokenの事)が取れる。なので、そのプロパティdetailsのプロパティhogeListでリストが取れる。あとはThymeleafのforeachなりなんなりで処理すれば良い。

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

やさC#(乱数)

C#では同じタイミングで2つ乱数が宣言された場合は同じ乱数しかでない。
要するに一秒違いで出た処理については別々の数値がでるといった・・・

何でそうしてるの?って仕組みが存在する

ここがjavaとは違うので比較する場合はつまづきやすい。

後程編集予定

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

やさC#(インデクサ)

過去に教えて頂いたインデクサをアウトプット

補足1

Javaにはない

Program.cs

using System;
using System.Collections.Generic;

namespace IndexerLesson
{
    public class Colors
    {
        private string[] data = { "赤", "青", "黄" };

         //アクセス 戻り値 this[型 引数]
        public string this[int index]{
            set{
                this.data[index] = value;
            }
            get{
                return data[index];
            }
        }

    }
}

Colors.cs

using System;
using System.Collections.Generic;

namespace IndexerLesson
{
    public class Colors
    {
        private string[] data = { "赤", "青", "黄" };

        public string this[int index]
        {
            set
            {
                this.data[index] = value;
            }
            get
            {
                return data[index];
            }
        }

    }
}

オーバーロード可能

JMonth.cs

using System;
using System.Collections.Generic;

namespace IndexerLesson
{
    public class JMonth
    {
        private string[] months = { "睦月", "如月", "弥生", "卯月", "皐月", "水無月", "文月", "葉月", "長月", "神無月", "霜月", "師走" };
        public string this[int index]
        {
            get
            {
                return months[index - 1];
            }
        }
        public int this[string name]
        {
            get
            {
                return Array.IndexOf(months, name) + 1;
            }
        }

    }
}

Profram.cs

using System;
using System.Collections.Generic;

namespace IndexerLesson
{
    class Class1
    {
        static void Main(string[] args)
        {
            JMonth jMonth = new JMonth();
            Console.WriteLine(jMonth[6]);
            Console.WriteLine(jMonth["神無月"]);
        }
    }
}

って認識です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

素数判定 Java

素数判定のコードです。
もっと効率のいいコードやきれいなコードがございましたら、アドバイスよろしくお願い致します。

import java.io.*;

class ファイル名{
        public static void main(String[]args) throws IOException
        {
            BufferedReader br =
                new BufferedReader(new InputStreamReader(System.in));

            System.out.print("正の整数を入力してください");
            String str = br.readLine();
            int a =Integer.parseInt(str);

            int b=1;

            while(b < a-1){
                b = b+1;
                if(a%b == 0) break;
            }
            if(a == 1 || a == 2) System.out.println("\n"+a+"は素数です");
            else if(a%b == 0)System.out.println("\n"+a+"は素数ではありません");
            else System.out.println("\n"+a+"は素数です");


        }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MyBatisでまとめてinsertする方法

概要

  • Listのデータをinsertしたいときに1件ずつinsertするのは無駄なのでデータをまとめて一括でinsertしたい
  • MyBatisGeneratorで自動生成されたMapperをカスタマイズした

注意事項

  • 再びgenerateしたら上書きされて消えるので要注意
  • IDがAUTO INCREMENTの場合はやり方が違うらしい

環境

  • Java8
    • SpringBoot v2.0.6.RELEASE
    • mybatis-spring-boot-starter v1.3.2
  • MySQL v5.7.25

方法

やることは2つ。
1. Mapperクラスに一括insertメソッドを定義する
2. xmlに一括insertの設定を追加する

例えばこんなテーブル(MySQL)があったとする。

user_friend.sql
CREATE TABLE user_friend (
  user_id int(10) NOT NULL,
  friend_id int(10) NOT NULL
  friend_name varchar(30) NOT NULL,
  created_at datetime NOT NULL,
  updated_at datetime NOT NULL,
  PRIMARY KEY (user_id, friend_id)
);

Mapperクラスにメソッドを追加する

  • insertBulkのメソッドを追加する
    • 引数にinsertしたいデータのListを渡す
    • @Paramで名前を付けるかどうかは任意
UserFriendMapper.java
int insertBulk(@Param("friendList")List<UserFriend> friendList);

xmlに設定を追加する

  • UserFriendMapper.xmlinsert id="insertBulk"を追加する
    • parameterType="java.util.List"に指定する
    • Mapperで@Paramを指定しなかった場合はcollection="list"と指定する
UserFriendMapper.xml
<!-- これを追加する -->
<insert id="insertBulk" parameterType="java.util.List">
    insert into user_friend
      (user_id, friend_id, friend_name, created_at, updated_at)
    values
    <foreach collection="friendList" item="fr" separator=","> 
    (
      #{fr.userId,jdbcType=INTEGER}, #{fr.friendId,jdbcType=INTEGER},
      #{fr.friendName,jdbcType=VARCHAR}, #{fr.createdAt,jdbcType=TIMESTAMP},
      #{fr.updatedAt,jdbcType=TIMESTAMP}
    )
    </foreach>
</insert>

Java(SpringBoot)側での使い方

Mapperクラスに追加したメソッドを呼び出すだけ。

FriendService.java
@Service
public class FriendService {

    @Autowired
    private UserFriendMapper userFriendMapper;

    /**
     * @return int insertした件数
     */
    public int insertFriends(int userId, List<UserFriend> saveFriends) {

        return userFriendMapper.insertBulk(saveFriends);
    }
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む