私は5年以上PHP開発に携わってきたバックエンドエンジニアです。
昨年から1年程度Java+Spring Frameworkで開発する案件に携わっておりましたので、そこで感じた言語や開発環境の差についてつらつらっと書きなぐってみました。

PHPは楽だけどここがあんまりだよね、Javaっていいよね というスタンスです。

ワカテのPHP技術者さんに見てもらいたいです。
理解が未熟な点はどうぞご指摘をお願いします。

言語の概要

PHPとは

PHPはLLと呼ばれる軽量なスクリプト言語です。

テンプレートエンジンとして生まれ機能が強化されてきました。
そのため、Webに特化にしており、最初に覚えることは比較的少ないと言えると思います。

また、過去の実績からまだまだ「セキュリティに弱い言語」というイメージが払しょくできていないのではないでしょうか。

また、「とりあえず何でも配列に突っ込む」など、簡単に悪い実装ができるため、設計や実装ルールをしっかり決めておくと、皆が幸せになれます。

Javaとは

Javaは守備範囲が広い汎用的な言語です。

プログラムをコンパイルしてバイトコードを生成し、JavaVM上で動作させます。

堅牢で信頼性が高く、シビアな要求がされるシステムで活躍する言語ですね。

メモリ消費が激しいとか、初回ロードに時間がかかってしまうイメージがありましたが、バージョンを追うごとに改善されていますね。

JREJDKJavaSEJavaEEなど、同じような単語がいくつも出てきて「どれをDLすればいいの?」と混乱したり、クラスパスに戸惑ったりと、初めに覚えることが多く、初心者に優しくない部類の言語だと思います。

また、ネット上にもJava入門がたくさんありますが、20周年を迎えたということもあり、検索で上位にヒットするサイトは古さを感じる大御所が多いような気がします。

Webサーバで実行する

PHP

コンパイルが不要で、.phpファイルをWebサーバ上にアップロードすればとりあえず動くという手軽さ。

Webサーバは「Apache+PHP」や「nginx+PHP-FPM」といった構成のサーバをあらかじめ用意しておきます。

「PHPをプログラムするエンジニア」と「”PHPを動作させるサーバ”を構築するエンジニア」が別担当者ということも珍しくなく、プログラム担当がサーバ側を全く知らないことも結構ある気がします。

Java

プログラムをパッケージ化Tomcatなどサーブレットコンテナ上に配置します。
デフォルトでは$CATALINA_HOME/webappsですね。

XMLファイルがいくつもあり、その複雑さに心が折れます。

依存ライブラリの管理

PHP

PHPの代表的なライブラリといえばPEARでしたが、昨今ではComposerが広く利用されるようになりました。

Composer
https://getcomposer.org/

Rubyにはbundlerというパッケージ管理ツールがありますが、ComposerはそのPHP版ですね。

composer.jsonにライブラリを追加してコマンドを打つとcomposerが自動でライブラリをDLしてオートロードを生成してくれます。

Composerは依存ライブラリをプロジェクト内部にDLし、そのプロジェクトだけで利用できるようにします。

PHPのinclude_pathに乗せるわけではありませんので、同じマシン内に複数バージョンのプロジェクトを並べることができます。

Java

私の参加したJavaプロジェクトではMavenを使用していました。

Apache Maven Project
https://maven.apache.org/

Mavenは依存ライブラリの管理、ビルド、再配布までを管理するというプロジェクト管理ツールですので、「パッケージ管理」という単語だと語弊がありますね。

Mavenはリポジトリを作って、その中でライブラリを管理しますので、例えばブランチごとにMavenリポジトリを分けて管理するといったことが必要になります。

言語的な差

変数宣言

ハッシュを宣言する場合のサンプルです。

PHPでは5.4からarray()ではなく[]で配列(下のコードは連想配列)を定義することができるようになりシンプルな記述が可能です。

<?php
$ids = [
    1 => 'user_a',
    2 => 'user_b',
];

すっきり。

Javaでは型宣言が必要なため、コード量が長くなってしまいます。

// Java
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(Integer.valueOf(1), "user_a");
map.put(Integer.valueOf(2), "user_b");

PHPに慣れていると、Javaの書き方がツラいなと感じる人は多いでしょう。
コード補完をうまく使いましょう。

また、Java5からオートボクシングが使える、Java7から右オペランドをnew HashMap<>()と省略できるようにはなり、書きやすくなってきてはいます。

余談ですが、こんなコードを見ると、古き良き時代のJavaだなぁと感じます。

StringBuilder  sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(is));

暗黙の型変換

Javaは静的型付けであり、文字列はあくまでも文字列として扱われます。
当然ですが。当たり前ですが。

// java
String strAge = "20";
strAge = strAge + 1; // "20"+ "1" = 201

これに対し、PHPは動的型付け勝手に型を変えて処理してしまうため、文字列が数字として扱われてしまうということがあります。

<?php
$age = "20";
var_dump($age);  // string(2) "20"

$age = $age + 1;
var_dump($age);  // int(21) <= 文字列20 + 数値1  201じゃないの?

文字列”20″が整数として扱われ、「20+1=21」という計算を行っています。
PHPerにしか理解できない挙動ですね。。。

では、文字列と文字列を演算してみます。

<?php
$age = "20";
$add = "1";
$age = $age + $add;
var_dump($age);  // int(21)

今度も21となりました。

$ageに文字列”a”を代入した場合どうでしょうか。

<?php
$age = "a";
$age = $age + 1;
var_dump($age);      // int(1) <= 文字列a + 数値1

“a”は数値として解釈できないので、0として扱われてしまいました。

これが暗黙の型変換です。
恐ろしいですね。

Javaで「文字列20 + 数値1」を21に導くためには、文字列を数値に変換します。

// java
String strAge = "20";
int age = Integer.parseInt(strAge);
age = age + 1; // 20 + 1 = 21

「自動で型変換が変換される」のは初心者に優しいだけで、もちろんバグの温床となります。

何がどうなるかをしっかり理解していれば対処可能ですが、対処が漏れても何となく動いてしまうため、厄介です。
PHPでは動的型付けを発生させないように型を意識するコーディングが大切です。

次のように、1つの変数に異なる型の値を代入できることも、コードを汚してわかりにくい不具合につながってしまいますね。

<?php
// アンチパターン
$tmp = true;
$tmp = "userName";

比較

PHPでは文字列をそのまま比較しますが、Javaでは当然アウトな実装です。

<?php
$strA = "AAA";
$strB = "AA";
$strB = $strB . "A";

if ( $strA == $strB ) { // true
:
// Java
String strA = "AAA";
String strB = "AA";
strB = strB + "A";

if (strA == strB) {  // false
:

if (strA.equals(strB)) {  // true
:

また、JavaでIntegerを比較する場合はintValue()を使用します。

Integer intA = new Integer(1);
Integer intB = new Integer(1);

if (intA == intB) {  // false
:

if (intA.intValue() == intB.intValue()) {  // true
:

上のコードの挙動は問題ないと思います。

しかし、大なり小なりで比較するときは次のような挙動となります。

Integer intA = new Integer(1);
Integer intC = new Integer(2);

if (intA == intC) { // false
:

if (intA < intC) {  // true
:

if (intA > intC) {  // false
:

==比較の比較はオブジェクトを比較し、> < の比較はアンボクシングされているのでしょう。
時々intValue()を省略する実装を目にしますが、わかりにくい動きなので、数値の比較はintValue()を明記し、プリミティブ型で比較を行うのが良いと考えています。

戻り型が不定

PHPは「返り値の型」指定が必須ではありません。
(Javaでは戻り型?)

そのため次のようなことができてしまいます。

<?php
// アンチパターン
public function foo($var)
{
    if ( [条件式1] ) {
        return "result";
    }
    if ( [条件式2] ) {
        return array();
    }
}
  • [条件式1]が真の場合は文字列を返す
  • [条件式2]が真の場合は配列を返す
  • [条件式1]、[条件式2]の両方が偽の場合は何も返却しない(void)

と、それぞれ異なる型を返却することができます。

“暗黙の型変換”と同様に、「期待している型」と「返却された型」が一致せずに不具合につながってしまいます。

これもJavaではありえない挙動です。

// java
public void foo() {
    return "foo"; // void メソッドは値を戻すことができません
}

PHP7になって型宣言(タイプヒンティングとも呼ばれる)が強化され、ようやくここで返り値の型を指定できるようになりました。

<?php
static function now(): DateTime
{
    return new DateTime();
}

PHP5.6以前のタイプヒントは引数のみ、型はarray、クラス型のみのです。
PHP7にぜひ移行しましょう。

参考

PHP > マニュアル > 言語リファレンス > 関数 > 返り値
http://php.net/manual/ja/functions.returning-values.php

PHP > マニュアル > 言語リファレンス > 関数 > 関数の引数
http://php.net/manual/ja/functions.arguments.php

配列、連想配列

とりあえず何でも配列に入れる という実装が簡単にできてしまうのがPHPの怖いところ。

<?php

function foo(array $a)
{
    // 何か処理
}

foo([ 1, 2, 3 ]);

foo([
    "key1" => "value1",
    "key2" => "value2",
]);

foo([
    "key1" => [ 1, 2, 3 ],
    "key2" => "value2",
]);

引数arrayで型宣言していますが、これらはすべて処理が通ります。

ここからPHPの配列がスカラー配列と連想配列の区別はなく、配列の内部構造がどうなっているかも考慮しないということがよくわかります。

公式にも「PHP の配列は、実際には順番付けられたマップです」という記述があります。

PHP > マニュアル > 言語リファレンス > 型
http://php.net/manual/ja/language.types.array.php

順序付きマップですからループしたときに順序がしっかり保証されるのですね。

<?php
$array = [
  "foo" => 1,
  "var" => 2,
];
foreach ($array as $key => $value) {
  echo "{$key} => {$value}";  // 必ず foo, varの順序で出力される
}

Javaで、PHPでいう連想想配と近いものがハッシュです。

// Java
Map<Integer, String> map = new HashMap<>();
map.put(1, "user1");
map.put(2, "user2");

for (Map.Entry<Integer, String> e : map.entrySet()) {
    System.out.println(e.getKey() + " : " + e.getValue());
}

次は配列。
Javaは配列宣言時に要素数を指定します。

// Java
int[] intArray = {1, 2, 3};

PHPの配列は後からいくらでも要素を追加できるので、PHPerはまずこの配列の扱いで転びます。

Listは柔軟にadd()できるため、PHPに近い動きをしてくれます。
拡張for文も扱いやすいです。

// Java
List<Integer> intList = new ArrayList<>();
idList.add(1);
idList.add(2);

for (Integer i : intList) {
:

タイムスタンプ

<?php
echo time();  // 1451574000
// Java
Date dt = new Date();
System.out.println(dt.getTime()); // 1451574000000

PHPでは秒単位であるのに対し、Javaではミリ秒単位でした。
よく忘れます。

例外処理

javaでは例外をスローする場合に未対処であれば、しっかり教えてくれます。

// Java
File file = new File(fileName);
FileReader filereader = new FileReader(file);
// 処理されない例外の型 FileNotFoundException

PHPでtry-catchが実装されたのはPHP5以降ということで、遅いですね。
それまではWarningやFatalといったエラーが発生してそこで処理がストップしてしまい、後処理をすることすら許してくれません。

書籍やネットのサンプルコードでも、しっかりと例外処理されていないものが多いため注意が必要です。

たとえばfile_get_contents()にURLを指定すればネット上のコンテンツを拾うことができますが、ネット接続に失敗したときにはfalseが返却されるだけです。

また、PHPでは関数の前に@をつけて、エラーを無視してしまうというかなり危険な処理も可能です。

<?php
$content = @file_get_contents($url);

PHPの実装が簡単にダメになる理由

配列での実装が楽なので何でも配列になりがち

本当にPHPでは配列ばかり使います。

たとえば、数年前に書かれたコードを読むと、DB問い合わせ結果を連想配列にfetchする実装が多いことに気が付きます。

$query  = $this->db->query($sql, $params);
$result = $query->fetchAll();

DB問い合わせ結果をprint_r();すると、次のような構造の配列になっています。

Array
(
    [0] => Array
        (
            [id]   => 1
            [name] => user1
        )

    [1] => Array
        (
            [id]   => 2
            [name] => user2
        )

)

「スカラー配列の中に連想配列が入っている」という構造ですね。
前述のとおり、スカラー配列も連想配列も両方arrayであるため、これではPHPの型宣言などで区別ができません。

次のコードは、過去非常によくお目にかかったPHPの実装です。

<?php
function getCardMst($id=null)
{
    $records = MCard::getInstance()->findAll();
    if ( is_null($id) )
    {
        // array( array(), array(), array()..  )
        return $records;
    }
    else
    {
        // array()
        return $records[$id];
    }
}

問題なのは引数の有無で返り値の構造が変わってしまうことです。
期待している返り値が得られなかった場合に意図しない動作となってしまいますね。

上記のPHPコードをJavaで書き直したサンプルが以下です。

// Java
public List<MChara> findAll() {
    return MChara.dao().findAll();
}

public MChara findOne(int id) {
  return MChara.dao().findOneById(id);
}

こちらは返り値の構造が一目瞭然であり、齟齬が少ないですね!

PHPでも、新しめの実装はORMを活用し、DB問い合わせ結果をクラスにマッピングしてくれるものがありますので、そういったものを活用しましょう。
Laravelはそうなってました。

PHPで返り値がどんどん汚れる例

<?php
function getRanking()
{
    return array(
        1 => 'user_a',
        2 => 'user_b',
        :
    );
}

ランキングの順位とユーザ名を返却するシンプルな関数があったとします。

「あ、ユーザIDも必要だな」ということで、ユーザIDを追加しました。
コードはこうなります。

<?php
function getRanking()
{
    return array(
        1 => array( 'name' => 'user_a', 'id' => 1111),
        2 => array( 'name' => 'user_b', 'id' => 2222),
        :
    );
}

「あ、ランキングの集計時間も欲しいな。」ということで、集計時間のデータを追加しました。

<?php
function getRanking()
{
    return array(
        "time" => "2015-01-01 17:17:00",
        "list" => array(
            1 => array( 'name' => 'user_a', 'id' => 1111),
            2 => array( 'name' => 'user_b', 'id' => 2222),
            :
        ),
    );
}

“集計時間”をどうやって返却するか悩んだ挙句、返り値の構造を大きく変えてしまいました。

運用フェーズに入って時間に追われるようになると、大真面目にこういった修正をやってしまいます。
だって、言語レベルでできちゃうから。

返り値が異なりますので、関数の呼び出し元も修正しなければなりません。
しかしコンパイルすることがありませんから、よく見落とし、サーバ上で動かした後に不具合が発覚します。

こんなことで不具合出しちゃうとか本当にもったないです。


Javaでは返り値の型を定義しなくてはなりません。
もし返り値の型や構造が変わる場合は呼び出し元も変更しなければコンパイルも通りません。

初期状態のコードをこのように実装したとします。

// Java
public Map<Integer, String> getRanking()
{
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "user_a");
    map.put(2, "user_b");
    :
    return map;
}

「ユーザIDの返却が必要」となった場合、小さなクラスを用意してMapの中身をそのクラスにすることで、きれいで把握しやすい構造とすることができますね。

// Java
public class RankingUser
{
    private String  name;
    private Integer id;

    public RankingUser(String name, Integer id) {
         this.name = name;
         this.id   = id;
    }
    :
}

public class FooService
{
    public Map<Integer, RankingUser> getRanking()
    {
        Map<Integer, RankingUser> map = new HashMap<>();

        map.put(1, new RankingUser("user1", 1111));
        map.put(2, new RankingUser("user2", 2222));
        :

        return map;
    }
}

おわりに

PHPは言語レベルでの制約が緩いため、簡単にコードを汚すことができます。

結果的にそれが修正コストを跳ね上げ、プロジェクトをコードを収集不可能な事態に追い込むことがありますから、不具合が減らせられるように意識したコーディングが大切です。

そのポイントは次にまとめられます。

  • 返り値の型を固定する
  • 配列で済まさずに、小さいクラスを構造体として用いる
  • しっかり例外対応を行う

PHPの悪いところをしっかり把握して実装でカバーしようという内容で、つまりPHPじゃなくてよくはないだろうかってことですね。

また、実装中は「Javaくそー」と思うこともあったのですが、Javaの良いところだけ見ているような内容になってしまいました。
何か思い出したら書き足します。

この記事はtomita@atuwebが書きました。



2016年05月16日:『比較』のPHPコードを修正しました。ご指摘くださいました方ありがとうございました。
2016年01月01日:記事をほぼ丸ごと書き直しました。

スポンサーリンク
ad_336
ad_336
  • このエントリーをはてなブックマークに追加
  • Evernoteに保存Evernoteに保存
スポンサーリンク
ad_336